CodeIgniter 3 Active Record (ORM) Standard Model supported Read & Write Connections. This package provide Base Model which extended CI_Model and provided full CRUD methods to make developing database interactions easier and quicker for your CodeIgniter applications.
<?php
namespace yidas;
use Exception;
/**
* Base Model
*
* @author Nick Tsai <myintaer@gmail.com>
* @version 2.17.1
* @see https://github.com/yidas/codeigniter-model
*/
class Model extends \CI_Model implements \ArrayAccess
{
/**
* Database Configuration for read-write master
*
* @var object|string|array CI DB ($this->db as default), CI specific group name or CI database config array
*/
protected $database = "";
/**
* Database Configuration for read-only slave
*
* @var object|string|array CI DB ($this->db as default), CI specific group name or CI database config array
*/
protected $databaseRead = "";
/**
* Table name
*
* @var string
*/
protected $table = "";
/**
* Table alias name
*
* @var string
*/
protected $alias = null;
/**
* Primary key of table
*
* @var string Field name of single column primary key
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
protected $timestamps = true;
/**
* Date format for timestamps.
*
* @var string unixtime|datetime
*/
protected $dateFormat = 'datetime';
/**
* @string Feild name for created_at, empty is disabled.
*/
const CREATED_AT = 'created_at';
/**
* @string Feild name for updated_at, empty is disabled.
*/
const UPDATED_AT = 'updated_at';
/**
* CREATED_AT triggers UPDATED_AT.
*
* @var bool
*/
protected $createdWithUpdated = true;
/**
* @var string Feild name for SOFT_DELETED, empty is disabled.
*/
const SOFT_DELETED = '';
/**
* The active value for SOFT_DELETED
*
* @var mixed
*/
protected $softDeletedFalseValue = '0';
/**
* The deleted value for SOFT_DELETED
*
* @var mixed
*/
protected $softDeletedTrueValue = '1';
/**
* This feature is actvied while having SOFT_DELETED
*
* @string Feild name for deleted_at, empty is disabled.
*/
const DELETED_AT = '';
/**
* @var array Validation errors (depends on validator driver)
*/
protected $_errors;
/**
* @var object database connection for write
*/
protected $_db;
/**
* @var object database connection for read (Salve)
*/
protected $_dbr;
/**
* @var object database caches by database key for write
*/
protected static $_dbCaches = [];
/**
* @var object database caches by database key for read (Salve)
*/
protected static $_dbrCaches = [];
/**
* @var object ORM schema caches by model class namespace
*/
private static $_ormCaches = [];
/**
* @var bool SOFT_DELETED one time switch
*/
private $_withoutSoftDeletedScope = false;
/**
* @var bool Global Scope one time switch
*/
private $_withoutGlobalScope = false;
/**
* ORM read properties
*
* @var array
*/
private $_readProperties = [];
/**
* ORM write properties
*
* @var array
*/
private $_writeProperties = [];
/**
* ORM self query
*
* @var string
*/
private $_selfCondition = null;
/**
* Clean next find one time setting
*
* @var boolean
*/
private $_cleanNextFind = false;
/**
* Constructor
*/
function __construct()
{
/* Database Connection Setting */
// Master
if ($this->database) {
if (is_object($this->database)) {
// CI DB Connection
$this->_db = $this->database;
}
elseif (is_string($this->database)) {
// Cache Mechanism
if (isset(self::$_dbCaches[$this->database])) {
$this->_db = self::$_dbCaches[$this->database];
} else {
// CI Database Configuration
$this->_db = get_instance()->load->database($this->database, true);
self::$_dbCaches[$this->database] = $this->_db;
}
}
else {
// Config array for each Model
$this->_db = get_instance()->load->database($this->database, true);
}
} else {
// CI Default DB Connection
$this->_db = $this->_getDefaultDB();
}
// Slave
if ($this->databaseRead) {
if (is_object($this->databaseRead)) {
// CI DB Connection
$this->_dbr = $this->databaseRead;
}
elseif (is_string($this->databaseRead)) {
// Cache Mechanism
if (isset(self::$_dbrCaches[$this->databaseRead])) {
$this->_dbr = self::$_dbrCaches[$this->databaseRead];
} else {
// CI Database Configuration
$this->_dbr = get_instance()->load->database($this->databaseRead, true);
self::$_dbrCaches[$this->databaseRead] = $this->_dbr;
}
}
else {
// Config array for each Model
$this->_dbr = get_instance()->load->database($this->databaseRead, true);
}
} else {
// CI Default DB Connection
$this->_dbr = $this->_getDefaultDB();
}
/* Table Name Guessing */
if (!$this->table) {
$this->table = str_replace('_model', '', strtolower(get_called_class()));
}
}
/**
* Get Master Database Connection
*
* @return object CI &DB
*/
public function getDatabase()
{
return $this->_db;
}
/**
* Get Slave Database Connection
*
* @return object CI &DB
*/
public function getDatabaseRead()
{
return $this->_dbr;
}
/**
* Alias of getDatabase()
*/
public function getDB()
{
return $this->getDatabase();
}
/**
* Alias of getDatabaseRead()
*/
public function getDBR()
{
return $this->getDatabaseRead();
}
/**
* Alias of getDatabaseRead()
*/
public function getBuilder()
{
return $this->getDatabaseRead();
}
/**
* Get table name
*
* @return string Table name
*/
public function getTable()
{
return $this->table;
}
/**
* Alias of getTable()
*/
public function tableName()
{
return $this->getTable();
}
/**
* Returns the filter rules for validation.
*
* @return array Filter rules. [[['attr1','attr2'], 'callable'],]
*/
public function filters()
{
return [];
}
/**
* Returns the validation rules for attributes.
*
* @see https://www.codeigniter.com/userguide3/libraries/form_validation.html#rule-reference
* @return array validation rules. (CodeIgniter Rule Reference)
*/
public function rules()
{
return [];
}
/**
* Performs the data validation with filters
*
* ORM only performs validation for assigned properties.
*
* @param array Data of attributes
* @param boolean Return filtered data
* @return boolean Result
* @return mixed Data after filter ($returnData is true)
*/
public function validate($attributes=[], $returnData=false)
{
// Data fetched by ORM or input
$data = ($attributes) ? $attributes : $this->_writeProperties;
// Filter first
$data = $this->filter($data);
// ORM re-assign properties
$this->_writeProperties = (!$attributes) ? $data : $this->_writeProperties;
// Get validation rules from function setting
$rules = $this->rules();
// The ORM update will only collect rules with corresponding modified attributes.
if ($this->_selfCondition) {
$newRules = [];
foreach ((array) $rules as $key => $rule) {
if (isset($this->_writeProperties[$rule['field']])) {
// Add into new rules for updating
$newRules[] = $rule;
}
}
// Replace with mapping rules
$rules = $newRules;
}
// Check if has rules
if (empty($rules))
return ($returnData) ? $data : true;
// CodeIgniter form_validation doesn't work with empty array data
if (empty($data))
return false;
// Load CodeIgniter form_validation library for yidas/model namespace, which has no effect on common one
get_instance()->load->library('form_validation', null, 'yidas_model_form_validation');
// Get CodeIgniter validator
$validator = get_instance()->yidas_model_form_validation;
$validator->reset_validation();
$validator->set_data($data);
$validator->set_rules($rules);
// Run Validate
$result = $validator->run();
// Result handle
if ($result===false) {
$this->_errors = $validator->error_array();
return false;
} else {
return ($returnData) ? $data : true;
}
}
/**
* Validation - Get error data referenced by last failed Validation
*
* @return array
*/
public function getErrors()
{
return $this->_errors;
}
/**
* Validation - Reset errors
*
* @return boolean
*/
public function resetErrors()
{
$this->_errors = null;
return true;
}
/**
* Filter process
*
* @param array $data Attributes
* @return array Filtered data
*/
public function filter($data)
{
// Get filter rules
$filters = $this->filters();
// Filter process with setting check
if (!empty($filters) && is_array($filters)) {
foreach ($filters as $key => $filter) {
if (!isset($filter[0]))
throw new Exception("No attributes defined in \$filters from " . get_called_class() . " (" . __CLASS__ . ")", 500);
if (!isset($filter[1]))
throw new Exception("No function defined in \$filters from " . get_called_class() . " (" . __CLASS__ . ")", 500);
list($attributes, $function) = $filter;
$attributes = (is_array($attributes)) ? $attributes : [$attributes];
// Filter each attribute
foreach ($attributes as $key => $attribute) {
if (!isset($data[$attribute]))
continue;
$data[$attribute] = call_user_func($function, $data[$attribute]);
}
}
}
return $data;
}
/**
* Set table alias for next find()
*
* @param string Table alias name
* @return self
*/
public function setAlias($alias)
{
$this->alias = $alias;
// Turn off cleaner to prevent continuous setting
$this->_cleanNextFind = false;
return $this;
}
/**
* Create an existent CI Query Builder instance with Model features for query purpose.
*
* @param boolean $withAll withAll() switch helper
* @return object CI_DB_query_builder
* @example
* $posts = $this->PostModel->find()
* ->where('is_public', '1')
* ->limit(0,25)
* ->order_by('id')
* ->get()
* ->result_array();
* @example
* // Without all featured conditions for next find()
* $posts = $this->PostModel->find(true)
* ->where('is_deleted', '1')
* ->get()
* ->result_array();
* // This is equal to withAll() method
* $this->PostModel->withAll()->find();
*
*/
public function find($withAll=false)
{
$instance = new static;
// One time setting reset mechanism
if ($instance->_cleanNextFind === true) {
// Reset alias
$instance->setAlias(null);
} else {
// Turn on clean for next find
$instance->_cleanNextFind = true;
}
// Alias option for FROM
$sqlFrom = ($instance->alias) ? "{$instance->table} AS {$instance->alias}" : $instance->table;
$instance->_dbr->from($sqlFrom);
// WithAll helper
if ($withAll===true) {
$instance->withAll();
}
// Scope condition
$instance->_addGlobalScopeCondition();
// Soft Deleted condition
$instance->_addSoftDeletedCondition();
return $instance->_dbr;
}
/**
* Create an CI Query Builder instance without Model Filters for query purpose.
*
* @return object CI_DB_query_builder
*/
public function forceFind()
{
return $this->withAll()->find();
}
/**
* Return a single active record model instance by a primary key or an array of column values.
*
* @param mixed $condition Refer to _findByCondition() for the explanation of this parameter
* @return object ActiveRecord(Model)
* @example
* $post = $this->Model->findOne(123);
* @example
* // Query builder ORM usage
* $this->Model->find()->where('id', 123);
* $this->Model->findOne();
*/
public static function findOne($condition=[])
{
$instance = new static;
$record = $instance->_findByCondition($condition)
->limit(1)
->get()->row_array();
// Record check
if (!$record) {
return $record;
}
return $instance->createActiveRecord($record, $record[$instance->primaryKey]);
}
/**
* Returns a list of active record models that match the specified primary key value(s) or a set of column values.
*
* @param mixed $condition Refer to _findByCondition() for the explanation
* @param integer|array $limit Limit or [offset, limit]
* @return array Set of ActiveRecord(Model)s
* @example
* $post = $this->PostModel->findAll([3,21,135]);
* @example
* // Query builder ORM usage
* $this->Model->find()->where_in('id', [3,21,135]);
* $this->Model->findAll();
*/
public static function findAll($condition=[], $limit=null)
{
$instance = new static;
$query = $instance->_findByCondition($condition);
// Limit / offset
if ($limit) {
$offset = null;
if (is_array($limit) && isset($limit[1])) {
// Prevent list() variable effect
$set = $limit;
list($offset, $limit) = $set;
}
$query = ($limit) ? $query->limit($limit) : $query;
$query = ($offset) ? $query->offset($offset) : $query;
}
$records = $query->get()->result_array();
// Record check
if (!$records) {
return $records;
}
$set = [];
// Each ActiveRecord
foreach ((array)$records as $key => $record) {
// Check primary key setting
if (!isset($record[$instance->primaryKey])) {
throw new Exception("Model's primary key not set", 500);
}
// Create an ActiveRecord into collect
$set[] = $instance->createActiveRecord($record, $record[$instance->primaryKey]);
}
return $set;
}
/**
* reset an CI Query Builder instance with Model.
*
* @return object Self
* @example
* $this->Model->reset()->find();
*/
public function reset()
{
// Reset query
$this->_db->reset_query();
$this->_dbr->reset_query();
return $this;
}
/**
* Insert a row with Timestamps feature into the associated database table using the attribute values of this record.
*
* @param array $attributes
* @param boolean $runValidation Whether to perform validation (calling validate()) before manipulate the record.
* @return boolean Result
* @example
* $result = $this->Model->insert([
* 'name' => 'Nick Tsai',
* 'email' => 'myintaer@gmail.com',
* ]);
*/
public function insert($attributes, $runValidation=true)
{
// Validation
if ($runValidation && false===$attributes=$this->validate($attributes, true))
return false;
$this->_attrEventBeforeInsert($attributes);
return $this->_db->insert($this->table, $attributes);
}
/**
* Insert a batch of rows with Timestamps feature into the associated database table using the attribute values of this record.
*
* @param array $data The rows to be batch inserted
* @param boolean $runValidation Whether to perform validation (calling validate()) before manipulate the record.
* @return int Number of rows inserted or FALSE on failure
* @example
* $result = $this->Model->batchInsert([
* ['name' => 'Nick Tsai', 'email' => 'myintaer@gmail.com'],
* ['name' => 'Yidas', 'email' => 'service@yidas.com']
* ]);
*/
public function batchInsert($data, $runValidation=true)
{
foreach ($data as $key => &$attributes) {
// Validation
if ($runValidation && false===$attributes=$this->validate($attributes, true))
return false;
$this->_attrEventBeforeInsert($attributes);
}
return $this->_db->insert_batch($this->table, $data);
}
/**
* Get the insert ID number when performing database inserts.
*
* @return integer Last insert ID
*/
public function getLastInsertID()
{
return $this->getDB()->insert_id();
}
/**
* Replace a row with Timestamps feature into the associated database table using the attribute values of this record.
*
* @param array $attributes
* @param boolean $runValidation Whether to perform validation (calling validate()) before manipulate the record.
* @return bool Result
* @example
* $result = $this->Model->replace([
* 'id' => 1,
* 'name' => 'Nick Tsai',
* 'email' => 'myintaer@gmail.com',
* ]);
*/
public function replace($attributes, $runValidation=true)
{
// Validation
if ($runValidation && false===$attributes=$this->validate($attributes, true))
return false;
$this->_attrEventBeforeInsert($attributes);
return $this->_db->replace($this->table, $attributes);
}
/**
* Save the changes with Timestamps feature to the selected record(s) into the associated database table.
*
* @param array $attributes
* @param mixed $condition Refer to _findByCondition() for the explanation
* @param boolean $runValidation Whether to perform validation (calling validate()) before manipulate the record.
* @return bool Result
*
* @example
* $this->Model->update(['status'=>'off'], 123)
* @example
* // Query builder ORM usage
* $this->Model->find()->where('id', 123);
* $this->Model->update(['status'=>'off']);
*/
public function update($attributes, $condition=NULL, $runValidation=true)
{
// Validation
if ($runValidation && false===$attributes=$this->validate($attributes, true))
return false;
// Model Condition
$query = $this->_findByCondition($condition);
$attributes = $this->_attrEventBeforeUpdate($attributes);
// Pack query then move it to write DB from read DB
$sql = $this->_dbr->set($attributes)->get_compiled_update();
$this->_dbr->reset_query();
return $this->_db->query($sql);
}
/**
* Update a batch of update queries into combined query strings.
*
* @param array $dataSet [[[Attributes], [Condition]], ]
* @param boolean $withAll withAll() switch helper
* @param integer $maxLenth MySQL max_allowed_packet
* @param boolean $runValidation Whether to perform validation (calling validate()) before manipulate the record.
* @return integer Count of successful query pack(s)
* @example
* $result = $this->Model->batchUpdate([
* [['title'=>'A1', 'modified'=>'1'], ['id'=>1]],
* [['title'=>'A2', 'modified'=>'1'], ['id'=>2]],
* ];);
*/
public function batchUpdate(Array $dataSet, $withAll=false, $maxLength=4*1024*1024, $runValidation=true)
{
$count = 0;
$sqlBatch = '';
foreach ($dataSet as $key => &$each) {
// Data format
list($attributes, $condition) = $each;
// Check attributes
if (!is_array($attributes) || !$attributes)
continue;
// Validation
if ($runValidation && false===$attributes=$this->validate($attributes, true))
continue;
// WithAll helper
if ($withAll===true) {
$this->withAll();
}
// Model Condition
$query = $this->_findByCondition($condition);
$attributes = $this->_attrEventBeforeUpdate($attributes);
// Pack query then move it to write DB from read DB
$sql = $this->_dbr->set($attributes)->get_compiled_update();
$this->_dbr->reset_query();
// Last batch check: First single query & Max length
// The first single query needs to be sent ahead to prevent the limitation that PDO transaction could not
// use multiple SQL line in one query, but allows if the multi-line query is behind a single query.
if (($count==0 && $sqlBatch) || strlen($sqlBatch)>=$maxLength) {
// Each batch of query
$result = $this->_db->query($sqlBatch);
$sqlBatch = "";
$count = ($result) ? $count + 1 : $count;
}
// Keep Combining query
$sqlBatch .= "{$sql};\n";
}
// Last batch of query
$result = $this->_db->query($sqlBatch);
return ($result) ? $count + 1 : $count;
}
/**
* Delete the selected record(s) with Timestamps feature into the associated database table.
*
* @param mixed $condition Refer to _findByCondition() for the explanation
* @param boolean $forceDelete Force to hard delete
* @param array $attributes Extended attributes for Soft Delete Mode
* @return bool Result
*
* @example
* $this->Model->delete(123);
* @example
* // Query builder ORM usage
* $this->Model->find()->where('id', 123);
* $this->Model->delete();
* @example
* // Force delete for SOFT_DELETED mode
* $this->Model->delete(123, true);
*/
public function delete($condition=NULL, $forceDelete=false, $attributes=[])
{
// Check is Active Record
if ($this->_readProperties) {
// Reset condition and find single by self condition
$this->reset();
$condition = $this->_selfCondition;
}
// Model Condition by $forceDelete switch
$query = ($forceDelete)
? $this->withTrashed()->_findByCondition($condition)
: $this->_findByCondition($condition);
/* Soft Delete Mode */
if (static::SOFT_DELETED
&& isset($this->softDeletedTrueValue)
&& !$forceDelete) {
// Mark the records as deleted
$attributes[static::SOFT_DELETED] = $this->softDeletedTrueValue;
$attributes = $this->_attrEventBeforeDelete($attributes);
// Pack query then move it to write DB from read DB
$sql = $this->_dbr->set($attributes)->get_compiled_update();
$this->_dbr->reset_query();
} else {
/* Hard Delete */
// Pack query then move it to write DB from read DB
$sql = $this->_dbr->get_compiled_delete();
$this->_dbr->reset_query();
}
return $this->_db->query($sql);
}
/**
* Force Delete the selected record(s) with Timestamps feature into the associated database table.
*
* @param mixed $condition Refer to _findByCondition() for the explanation
* @return mixed CI delete result of DB Query Builder
*
* @example
* $this->Model->forceDelete(123)
* @example
* // Query builder ORM usage
* $this->Model->find()->where('id', 123);
* $this->Model->forceDelete();
*/
public function forceDelete($condition=NULL)
{
return $this->delete($condition, true);
}
/**
* Get the number of affected rows when doing “write” type queries (insert, update, etc.).
*
* @return integer Last insert ID
*/
public function getAffectedRows()
{
return $this->getDB()->affected_rows();
}
/**
* Restore SOFT_DELETED field value to the selected record(s) into the associated database table.
*
* @param mixed $condition Refer to _findByCondition() for the explanation
* @return bool Result
*
* @example
* $this->Model->restore(123)
* @example
* // Query builder ORM usage
* $this->Model->withTrashed()->find()->where('id', 123);
* $this->Model->restore();
*/
public function restore($condition=NULL)
{
// Model Condition with Trashed
$query = $this->withTrashed()->_findByCondition($condition);
/* Soft Delete Mode */
if (static::SOFT_DELETED
&& isset($this->softDeletedFalseValue)) {
// Mark the records as deleted
$attributes[static::SOFT_DELETED] = $this->softDeletedFalseValue;
return $query->update($this->table, $attributes);
} else {
return false;
}
}
/**
* Get count from query
*
* @param boolean Reset query conditions
* @return integer
*/
public function count($resetQuery=true)
{
return $this->getDBR()->count_all_results('', $resetQuery);
}
/**
* Lock the selected rows in the table for updating.
*
* sharedLock locks only for write, lockForUpdate also prevents them from being selected
*
* @example
* $this->Model->find()->where('id', 123)
* $result = $this->Model->lockForUpdate()->row_array();
* @example
* // This transaction block will lock selected rows for next same selected
* // rows with `FOR UPDATE` lock:
* $this->Model->getDB()->trans_start();
* $this->Model->find()->where('id', 123)
* $result = $this->Model->lockForUpdate()->row_array();
* $this->Model->getDB()->trans_complete();
*
* @return object CI_DB_result
*/
public function lockForUpdate()
{
// Pack query then move it to write DB from read DB for transaction
$sql = $this->_dbr->get_compiled_select();
$this->_dbr->reset_query();
return $this->_db->query("{$sql} FOR UPDATE");
}
/**
* Share lock the selected rows in the table.
*
* @example
* $this->Model->find()->where('id', 123)
* $result = $this->Model->sharedLock()->row_array();'
*
* @return object CI_DB_result
*/
public function sharedLock()
{
// Pack query then move it to write DB from read DB for transaction
$sql = $this->_dbr->get_compiled_select();
$this->_dbr->reset_query();
return $this->_db->query("{$sql} LOCK IN SHARE MODE");
}
/**
* Without SOFT_DELETED query conditions for next find()
*
* @return object Self
* @example
* $this->Model->withTrashed()->find();
*/
public function withTrashed()
{
$this->_withoutSoftDeletedScope = true;
return $this;
}
/**
* Without Global Scopes query conditions for next find()
*
* @return object Self
* @example
* $this->Model->withoutGlobalScopes()->find();
*/
public function withoutGlobalScopes()
{
$this->_withoutGlobalScope = true;
return $this;
}
/**
* Without all query conditions for next find()
* That is, with all set of Models for next find()
*
* @return object Self
* @example
* $this->Model->withAll()->find();
*/
public function withAll()
{
// Turn off switches of all featured conditions
$this->withTrashed();
$this->withoutGlobalScopes();
return $this;
}
/**
* New a Active Record from Model by data
*
* @param array $readProperties
* @param array $selfCondition
* @return object ActiveRecord(Model)
*/
public function createActiveRecord($readProperties, $selfCondition)
{
$activeRecord = new static();
// ORM handling
$activeRecord->_readProperties = $readProperties;
// Primary key condition to ensure single query result
$activeRecord->_selfCondition = $selfCondition;
return $activeRecord;
}
/**
* Active Record (ORM) save for insert or update
*
* @param boolean $runValidation Whether to perform validation (calling validate()) before manipulate the record.
* @return bool Result of CI insert
*/
public function save($runValidation=true)
{
// if (empty($this->_writeProperties))
// return false;
// ORM status distinguishing
if (!$this->_selfCondition) {
// Event
if (!$this->beforeSave(true)) {
return false;
}
$result = $this->insert($this->_writeProperties, $runValidation);
// Change this ActiveRecord to update mode
if ($result) {
// ORM handling
$this->_readProperties = $this->_writeProperties;
$insertID = $this->getLastInsertID();
$this->_readProperties[$this->primaryKey] = $insertID;
$this->_selfCondition = $insertID;
// Event
$this->afterSave(true, $this->_readProperties);
// Reset properties
$this->_writeProperties = [];
}
} else {
// Event
if (!$this->beforeSave(false)) {
return false;
}
$result = ($this->_writeProperties) ? $this->update($this->_writeProperties, $this->_selfCondition, $runValidation) : true;
// Check the primary key is changed
if ($result) {
// Primary key condition to ensure single query result
if (isset($this->_writeProperties[$this->primaryKey])) {
$this->_selfCondition = $this->_writeProperties[$this->primaryKey];
}
$this->_readProperties = array_merge($this->_readProperties, $this->_writeProperties);
// Event
$this->afterSave(true, $this->_readProperties);
// Reset properties
$this->_writeProperties = [];
}
}
return $result;
}
/**
* This method is called at the beginning of inserting or updating a active record
*
* @param bool $insert whether this method called while inserting a record.
* If `false`, it means the method is called while updating a record.
* @return bool whether the insertion or updating should continue.
* If `false`, the insertion or updating will be cancelled.
*/
public function beforeSave($insert)
{
// overriding
return true;
}
/**
* This method is called at the end of inserting or updating a active record
*
* @param bool $insert whether this method called while inserting a record.
* If `false`, it means the method is called while updating a record.
* @param array $changedAttributes The old values of attributes that had changed and were saved.
* You can use this parameter to take action based on the changes made for example send an email
* when the password had changed or implement audit trail that tracks all the changes.
* `$changedAttributes` gives you the old attribute values while the active record (`$this`) has
* already the new, updated values.
*/
public function afterSave($insert, $changedAttributes)
{
// overriding
}
/**
* Declares a has-many relation.
*
* @param string $modelName The model class name of the related record
* @param string $foreignKey
* @param string $localKey
* @return object CI_DB_query_builder
*/
public function hasMany($modelName, $foreignKey=null, $localKey=null)
{
return $this->_relationship($modelName, __FUNCTION__, $foreignKey, $localKey);
}
/**
* Declares a has-many relation.
*
* @param string $modelName The model class name of the related record
* @param string $foreignKey
* @param string $localKey
* @return object CI_DB_query_builder
*/
public function hasOne($modelName, $foreignKey=null, $localKey=null)
{
return $this->_relationship($modelName, __FUNCTION__, $foreignKey, $localKey);
}
/**
* Base relationship.
*
* @param string $modelName The model class name of the related record
* @param string $relationship
* @param string $foreignKey
* @param string $localKey
* @return object CI_DB_query_builder
*/
protected function _relationship($modelName, $relationship, $foreignKey=null, $localKey=null)
{
/**
* PSR-4 support check
*
* @see https://github.com/yidas/codeigniter-psr4-autoload
*/
if (strpos($modelName, "\\") !== false ) {
$model = new $modelName;
} else {
// Original CodeIgniter 3 model loader
get_instance()->load->model($modelName);
$model = $this->$modelName;
}
$libClass = __CLASS__;
// Check if is using same library
if (!is_subclass_of($model, $libClass)) {
throw new Exception("Model `{$modelName}` does not extend {$libClass}", 500);
}
// Keys
$foreignKey = ($foreignKey) ? $foreignKey : $this->primaryKey;
$localKey = ($localKey) ? $localKey : $this->primaryKey;
$query = $model->find()
->where($foreignKey, $this->$localKey);
// Inject Model name into query builder for ORM relationships
$query->modelName = $modelName;
// Inject relationship type into query builder for ORM relationships
$query->relationship = $relationship;
return $query;
}
/**
* Active Record transform to array record
*
* @return array
* @example $record = $activeRecord->toArray();
*/
public function toArray()
{
return $this->_readProperties;
}
/**
* Index by Key
*
* @param array $array Array data for handling
* @param string $key Array key for index key
* @param bool $obj2Array Object converts to array if is object
* @return array Result with indexBy Key
* @example
* $records = $this->Model->findAll();
* $this->Model->indexBy($records, 'sn');
*/
public static function indexBy(Array &$array, $key=null, $obj2Array=false)
{
// Use model instance's primary key while no given key
$key = ($key) ?: (new static())->primaryKey;
$tmp = [];
foreach ($array as $row) {
// Array & Object types support
if (is_object($row) && isset($row->$key)) {
$tmp[$row->$key] = ($obj2Array) ? (array)$row : $row;
}
elseif (is_array($row) && isset($row[$key])) {
$tmp[$row[$key]] = $row;
}
}
return $array = $tmp;
}
/**
* Encodes special characters into HTML entities.
*
* The [[$this->config->item('charset')]] will be used for encoding.
*
* @param string $content the content to be encoded
* @param bool $doubleEncode whether to encode HTML entities in `$content`. If false,
* HTML entities in `$content` will not be further encoded.
* @return string the encoded content
*
* @see http://www.php.net/manual/en/function.htmlspecialchars.php
* @see https://www.yiiframework.com/doc/api/2.0/yii-helpers-basehtml#encode()-detail
*/
public static function htmlEncode($content, $doubleEncode = true)
{
$ci = & get_instance();
return htmlspecialchars($content, ENT_QUOTES | ENT_SUBSTITUTE, $ci->config->item('charset') ? $ci->config->item('charset') : 'UTF-8', $doubleEncode);
}
/**
* Decodes special HTML entities back to the corresponding characters.
*
* This is the opposite of [[encode()]].
*
* @param string $content the content to be decoded
* @return string the decoded content
* @see htmlEncode()
* @see http://www.php.net/manual/en/function.htmlspecialchars-decode.php
* @see https://www.yiiframework.com/doc/api/2.0/yii-helpers-basehtml#decode()-detail
*/
public static function htmlDecode($content)
{
return htmlspecialchars_decode($content, ENT_QUOTES);
}
/**
* Query Scopes Handler
*
* @return bool Result
*/
protected function _globalScopes()
{
// Events for inheriting
return true;
}
/**
* Attributes handle function for each Insert
*
* @param array $attributes
* @return array Addon $attributes of pointer
*/
protected function _attrEventBeforeInsert(&$attributes)
{
$this->_formatDate(static::CREATED_AT, $attributes);
// Trigger UPDATED_AT
if ($this->createdWithUpdated) {
$this->_formatDate(static::UPDATED_AT, $attributes);
}
return $attributes;
}
/**
* Attributes handle function for Update
*
* @param array $attributes
* @return array Addon $attributes of pointer
*/
protected function _attrEventBeforeUpdate(&$attributes)
{
$this->_formatDate(static::UPDATED_AT, $attributes);
return $attributes;
}
/**
* Attributes handle function for Delete
*
* @param array $attributes
* @return array Addon $attributes of pointer
*/
protected function _attrEventBeforeDelete(&$attributes)
{
$this->_formatDate(static::DELETED_AT, $attributes);
return $attributes;
}
/**
* Finds record(s) by the given condition with a fresh query.
*
* This method is internally called by findOne(), findAll(), update(), delete(), etc.
* The query will be reset to start a new scope if the condition is used.
*
* @param mixed Primary key value or a set of column values. If is null, it would be used for
* previous find() method, which means it would not rebuild find() so it would check and
* protect the SQL statement.
* @return object CI_DB_query_builder
* @internal
* @example
* // find a single customer whose primary key value is 10
* $this->_findByCondition(10);
*
* // find the customers whose primary key value is 10, 11 or 12.
* $this->_findByCondition([10, 11, 12]);
*
* // find the first customer whose age is 30 and whose status is 1
* $this->_findByCondition(['age' => 30, 'status' => 1]);
*/
protected function _findByCondition($condition=null)
{
// Reset Query if condition existed
if ($condition !== null) {
$this->_dbr->reset_query();
$query = $this->find();
} else {
// Support for previous find(), no need to find() again
$query = $this->_dbr;
}
// Check condition type
if (is_array($condition)) {
// Check if is numeric array
if (array_keys($condition)===range(0, count($condition)-1)) {
/* Numeric Array */
$query->where_in($this->_field($this->primaryKey), $condition);
} else {
/* Associated Array */
foreach ($condition as $field => $value) {
(is_array($value)) ? $query->where_in($field, $value) : $query->where($field, $value);
}
}
}
elseif (is_numeric($condition) || is_string($condition)) {
/* Single Primary Key */
$query->where($this->_field($this->primaryKey), $condition);
}
else {
// Simply Check SQL for no condition such as update/delete
// Warning: This protection just simply check keywords that may not find out for some situations.
$sql = $this->_dbr->get_compiled_select('', false); // No reset query
// Check FROM for table condition
if (stripos($sql, 'from ')===false)
throw new Exception("You should find() first, or use condition array for update/delete", 400);
// No condition situation needs to enable where protection
if (stripos($sql, 'where ')===false)
throw new Exception("You could not update/delete without any condition! Use find()->where('1=1') or condition array at least.", 400);
}
return $query;
}
/**
* Format a date for timestamps
*
* @param string Field name
* @param array Attributes
* @return array Addon $attributes of pointer
*/
protected function _formatDate($field, &$attributes)
{
if ($this->timestamps && $field) {
switch ($this->dateFormat) {
case 'datetime':
$dateFormat = date("Y-m-d H:i:s");
break;
case 'unixtime':
default:
$dateFormat = time();
break;
}
$attributes[$field] = $dateFormat;
}
return $attributes;
}
/**
* The scope which not been soft deleted
*
* @param bool $skip Skip
* @return bool Result
*/
protected function _addSoftDeletedCondition()
{
if ($this->_withoutSoftDeletedScope) {
// Reset SOFT_DELETED switch
$this->_withoutSoftDeletedScope = false;
}
elseif (static::SOFT_DELETED && isset($this->softDeletedFalseValue)) {
// Add condition
$this->_dbr->where($this->_field(static::SOFT_DELETED),
$this->softDeletedFalseValue);
}
return true;
}
/**
* The scope which not been soft deleted
*
* @param bool $skip Skip
* @return bool Result
*/
protected function _addGlobalScopeCondition()
{
if ($this->_withoutGlobalScope) {
// Reset Global Switch switch
$this->_withoutGlobalScope = false;
} else {
// Default to apply global scopes
$this->_globalScopes();
}
return true;
}
/**
* Standardize field name
*
* @param string $columnName
* @return string Standardized column name
*/
protected function _field($columnName)
{
return ($this->alias) ? "`{$this->alias}`.`{$columnName}`" : "`{$this->table}`.`{$columnName}`";
}
/**
* Get & load $this->db in CI application
*
* @return object CI $this->db
*/
private function _getDefaultDB()
{
// For ReadDatabase checking Master first
if ($this->_db) {
return $this->_db;
}
if (!isset($this->db)) {
get_instance()->load->database();
}
// No need to set as reference because $this->db is refered to &DB already.
return get_instance()->db;
}
/**
* ORM set property
*
* @param string $name Property key name
* @param mixed $value
*/
public function __set($name, $value)
{
$this->_writeProperties[$name] = $value;
}
/**
* ORM get property
*
* @param string $name Property key name
*/
public function __get($name)
{
// ORM property check
if (array_key_exists($name, $this->_writeProperties) ) {
return $this->_writeProperties[$name];
}
else if (array_key_exists($name, $this->_readProperties)) {
return $this->_readProperties[$name];
}
// ORM relationship check
else if (method_exists($this, $method = $name)) {
$query = call_user_func_array([$this, $method], []);
// Extract query builder injection property
$modelName = isset($query->modelName) ? $query->modelName : null;
$relationship = isset($query->relationship) ? $query->relationship : null;
if (!$modelName || !$relationship) {
throw new Exception("ORM relationships error", 500);
}
/**
* PSR-4 support check
*
* @see https://github.com/yidas/codeigniter-psr4-autoload
*/
if (strpos($modelName, "\\") !== false ) {
$model = new $modelName;
} else {
// Original CodeIgniter 3 model loader
get_instance()->load->model($modelName);
$model = $this->$modelName;
}
// Check return type
if ($relationship == 'hasOne') {
// Keep same query builder from hasOne()
return $model->findOne(null);
} else {
// Keep same query builder from hasMany()
return $model->findAll(null);
}
}
// ORM schema check
else {
$class = get_class($this);
// Check ORM Schema cache
if (!isset(self::$_ormCaches[$class])) {
$columns = $this->_dbr->query("SHOW COLUMNS FROM `{$this->table}`;")
->result_array();
// Cache
self::$_ormCaches[get_class($this)] = $columns;
}
// Write cache to read properties of this ORM
foreach (self::$_ormCaches[get_class($this)] as $key => $column) {
$this->_readProperties[$column['Field']] = isset($this->_readProperties[$column['Field']])
? $this->_readProperties[$column['Field']]
: null;
}
// Match property again
if (array_key_exists($name, $this->_readProperties)) {
return $this->_readProperties[$name];
}
// CI parent::__get() check
if (property_exists(get_instance(), $name)) {
return parent::__get($name);
}
// Exception
throw new \Exception("Property `{$name}` does not exist", 500);
}
return null;
}
/**
* ORM isset property
*
* @param string $name
* @return void
*/
public function __isset($name) {
if (isset($this->_writeProperties[$name])) {
return true;
}
return isset($this->_readProperties[$name]);
}
/**
* ORM unset property
*
* @param string $name
* @return void
*/
public function __unset($name) {
unset($this->_writeProperties[$name]);
unset($this->_readProperties[$name]);
}
/**
* ArrayAccess offsetSet
*
* @param string $offset
* @param mixed $value
* @return void
*/
public function offsetSet($offset, $value) {
return $this->__set($offset, $value);
}
/**
* ArrayAccess offsetExists
*
* @param string $offset
* @return bool Result
*/
public function offsetExists($offset) {
return $this->__isset($offset);
}
/**
* ArrayAccess offsetUnset
*
* @param string $offset
* @return void
*/
public function offsetUnset($offset) {
return $this->__unset($offset);
}
/**
* ArrayAccess offsetGet
*
* @param string $offset
* @return mixed Value of property
*/
public function offsetGet($offset) {
if (isset($this->_writeProperties[$offset])) {
return $this->_writeProperties[$offset];
}
elseif (isset($this->_readProperties[$offset]) ) {
return $this->_readProperties[$offset];
}
else {
// Trace debug
$lastFile = debug_backtrace()[0]['file'];
$lastLine = debug_backtrace()[0]['line'];
trigger_error("Undefined index: " . get_called_class() . "->{$offset} called by {$lastFile}:{$lastLine}", E_USER_NOTICE);
return null;
}
}
}