SQL functions: * @method DataMapper where_field_field_func() where_field_func($field, string $function_name, mixed $args,...) Limits results based on a SQL function. * @method DataMapper or_where_field_field_func() or_where_field_func($field, string $function_name, mixed $args,...) Limits results based on a SQL function, via OR. * @method DataMapper where_in_field_field_func() where_in_field_func($field, string $function_name, mixed $args,...) Limits results by comparing a SQL function to a range of values. * @method DataMapper or_where_in_field_field_func() or_where_in_field_func($field, string $function_name, mixed $args,...) Limits results by comparing a SQL function to a range of values. * @method DataMapper where_not_in_field_field_func() where_not_in_field_func($field, string $function_name, string $field) Limits results by comparing a SQL function to a range of values. * @method DataMapper or_where_not_in_field_field_func() or_where_not_in_field_func($field, string $function_name, mixed $args,...) Limits results by comparing a SQL function to a range of values. * @method DataMapper like_field_field_func() like_field_func($field, string $function_name, mixed $args,...) Limits results by matching a SQL function to a value. * @method DataMapper or_like_field_field_func() like_field_func($field, string $function_name, mixed $args,...) Limits results by matching a SQL function to a value. * @method DataMapper not_like_field_field_func() like_field_func($field, string $function_name, mixed $args,...) Limits results by matching a SQL function to a value. * @method DataMapper or_not_like_field_field_func() like_field_func($field, string $function_name, mixed $args,...) Limits results by matching a SQL function to a value. * @method DataMapper ilike_field_field_func() like_field_func($field, string $function_name, mixed $args,...) Limits results by matching a SQL function to a value (case insensitive). * @method DataMapper or_ilike_field_field_func() like_field_func($field, string $function_name, mixed $args,...) Limits results by matching a SQL function to a value (case insensitive). * @method DataMapper not_ilike_field_field_func() like_field_func($field, string $function_name, mixed $args,...) Limits results by matching a SQL function to a value (case insensitive). * @method DataMapper or_not_ilike_field_field_func() like_field_func($field, string $function_name, mixed $args,...) Limits results by matching a SQL function to a value (case insensitive). * @method DataMapper group_by_field_field_func() group_by_field_func($field, string $function_name, mixed $args,...) Groups the query by a SQL function. * @method DataMapper having_field_field_func() having_field_func($field, string $function_name, mixed $args,...) Groups the querying using a HAVING clause. * @method DataMapper or_having_field_field_func() having_field_func($field, string $function_name, mixed $args,...) Groups the querying using a HAVING clause, via OR. * @method DataMapper order_by_field_field_func() order_by_field_func($field, string $function_name, mixed $args,...) Orders the query based on a SQL function. * * Subqueries: * @method DataMapper select_subquery() select_subquery(DataMapper $subquery, string $alias) Selects the result of a function. Alias is required. * @method DataMapper where_subquery() where_subquery(mixed $subquery_or_field, mixed $value_or_subquery) Limits results based on a subquery. * @method DataMapper or_where_subquery() or_where_subquery(mixed $subquery_or_field, mixed $value_or_subquery) Limits results based on a subquery, via OR. * @method DataMapper where_in_subquery() where_in_subquery(mixed $subquery_or_field, mixed $values_or_subquery) Limits results by comparing a subquery to a range of values. * @method DataMapper or_where_in_subquery() or_where_in_subquery(mixed $subquery_or_field, mixed $values_or_subquery) Limits results by comparing a subquery to a range of values. * @method DataMapper where_not_in_subquery() where_not_in_subquery(mixed $subquery_or_field, string $field, mixed $values_or_subquery) Limits results by comparing a subquery to a range of values. * @method DataMapper or_where_not_in_subquery() or_where_not_in_subquery(mixed $subquery_or_field, mixed $values_or_subquery) Limits results by comparing a subquery to a range of values. * @method DataMapper like_subquery() like_subquery(DataMapper $subquery, string $value, string $match = 'both') Limits results by matching a subquery to a value. * @method DataMapper or_like_subquery() like_subquery(DataMapper $subquery, string $value, string $match = 'both') Limits results by matching a subquery to a value. * @method DataMapper not_like_subquery() like_subquery(DataMapper $subquery, string $value, string $match = 'both') Limits results by matching a subquery to a value. * @method DataMapper or_not_like_subquery() like_subquery(DataMapper $subquery, string $value, string $match = 'both') Limits results by matching a subquery to a value. * @method DataMapper ilike_subquery() like_subquery(DataMapper $subquery, string $value, string $match = 'both') Limits results by matching a subquery to a value (case insensitive). * @method DataMapper or_ilike_subquery() like_subquery(DataMapper $subquery, string $value, string $match = 'both') Limits results by matching a subquery to a value (case insensitive). * @method DataMapper not_ilike_subquery() like_subquery(DataMapper $subquery, string $value, string $match = 'both') Limits results by matching a subquery to a value (case insensitive). * @method DataMapper or_not_ilike_subquery() like_subquery(DataMapper $subquery, string $value, string $match = 'both') Limits results by matching a subquery to a value (case insensitive). * @method DataMapper having_subquery() having_subquery(string $field, DataMapper $subquery) Groups the querying using a HAVING clause. * @method DataMapper or_having_subquery() having_subquery(string $field, DataMapper $subquery) Groups the querying using a HAVING clause, via OR. * @method DataMapper order_by_subquery() order_by_subquery(DataMapper $subquery, string $direction) Orders the query based on a subquery. * * Related Subqueries: * @method DataMapper where_related_subquery() where_related_subquery(mixed $related_model, string $related_field, DataMapper $subquery) Limits results based on a subquery. * @method DataMapper or_where_related_subquery() or_where_related_subquery(mixed $related_model, string $related_field, DataMapper $subquery) Limits results based on a subquery, via OR. * @method DataMapper where_in_related_subquery() where_in_related_subquery(mixed $related_model, string $related_field, DataMapper $subquery) Limits results by comparing a subquery to a range of values. * @method DataMapper or_where_in_related_subquery() or_where_in_related_subquery(mixed $related_model, string $related_field, DataMapper $subquery) Limits results by comparing a subquery to a range of values. * @method DataMapper where_not_in_related_subquery() where_not_in_related_subquery(mixed $related_model, string $related_field, DataMapper $subquery) Limits results by comparing a subquery to a range of values. * @method DataMapper or_where_not_in_related_subquery() or_where_not_in_related_subquery(mixed $related_model, string $related_field, DataMapper $subquery) Limits results by comparing a subquery to a range of values. * @method DataMapper having_related_subquery() having_related_subquery(mixed $related_model, string $related_field, DataMapper $subquery) Groups the querying using a HAVING clause. * @method DataMapper or_having_related_subquery() having_related_subquery(mixed $related_model, string $related_field, DataMapper $subquery) Groups the querying using a HAVING clause, via OR. * * Array Extension: * @method array to_array() to_array($fields = '') NEEDS ARRAY EXTENSION. Converts this object into an associative array. @link DMZ_Array::to_array * @method array all_to_array() all_to_array($fields = '') NEEDS ARRAY EXTENSION. Converts the all array into an associative array. @link DMZ_Array::all_to_array * @method array|bool from_array() from_array($data, $fields = '', $save = FALSE) NEEDS ARRAY EXTENSION. Converts $this->all into an associative array. @link DMZ_Array::all_to_array * * CSV Extension * @method bool csv_export() csv_export($filename, $fields = '', $include_header = TRUE) NEEDS CSV EXTENSION. Exports this object as a CSV file. * @method array csv_import() csv_import($filename, $fields = '', $header_row = TRUE, $callback = NULL) NEEDS CSV EXTENSION. Imports a CSV file into this object. * * JSON Extension: * @method string to_json() to_json($fields = '', $pretty_print = FALSE) NEEDS JSON EXTENSION. Converts this object into a JSON string. * @method string all_to_json() all_to_json($fields = '', $pretty_print = FALSE) NEEDS JSON EXTENSION. Converts the all array into a JSON string. * @method bool from_json() from_json($json, $fields = '') NEEDS JSON EXTENSION. Imports the values from a JSON string into this object. * @method void set_json_content_type() set_json_content_type() NEEDS JSON EXTENSION. Sets the content type header to Content-Type: application/json. * * SimpleCache Extension: * @method DataMapper get_cached() get_cached($limit = '', $offset = '') NEEDS SIMPLECACHE EXTENSION. Enables cacheable queries. * @method DataMapper clear_cache() get_cached($segment,...) NEEDS SIMPLECACHE EXTENSION. Clears a cache for the specfied segment. * * Translate Extension: * * Nestedsets Extension: * */ class DataMapper implements IteratorAggregate { /** * Stores the shared configuration * @var array */ static $config = array(); /** * Stores settings that are common across a specific Model * @var array */ static $common = array(DMZ_CLASSNAMES_KEY => array()); /** * Stores global extensions * @var array */ static $global_extensions = array(); /** * Used to override unset default properties. * @var array */ static $_dmz_config_defaults = array( 'timestamp_format' => 'Y-m-d H:i:s O', 'created_field' => 'created', 'updated_field' => 'updated', 'extensions_path' => 'datamapper', 'field_label_lang_format' => '${model}_${field}', ); /** * Contains any errors that occur during validation, saving, or other * database access. * @var DM_Error_Object */ public $error; /** * Used to keep track of the original values from the database, to * prevent unecessarily changing fields. * @var object */ public $stored; /** * DB Table Prefix * @var string */ public $prefix = ''; /** * DB Join Table Prefix * @var string */ public $join_prefix = ''; /** * The name of the table for this model (may be automatically generated * from the classname). * @var string */ public $table = ''; /** * The singular name for this model (may be automatically generated from * the classname). * @var string */ public $model = ''; /** * Can be used to override the default database behavior. * @var mixed */ public $db_params = ''; /** * Prefix string used when reporting errors. * @var string */ public $error_prefix = ''; /** * Suffic string used when reporting errors. * @var string */ public $error_suffix = ''; /** * Custom name for the automatic timestamp saved with new objects. * Defaults to 'created'. * @var string */ public $created_field = ''; /** * Custom name for the automatic timestamp saved when an object changes. * Defaults to 'updated'. * @var string */ public $updated_field = ''; /** * If TRUE, automatically wrap every save and delete in a transaction. * @var bool */ public $auto_transaction = FALSE; /** * If TRUE, has_many relationships are automatically loaded when accessed. * Not recommended in most situations. * @var bool */ public $auto_populate_has_many = FALSE; /** * If TRUE, has_one relationships are automatically loaded when accessed. * Not recommended in some situations. * @var bool */ public $auto_populate_has_one = FALSE; /** * Enables the old method of storing the all array using an object's ID. * @var bool */ public $all_array_uses_ids = FALSE; /** * The result of validate is stored here. * @var bool */ public $valid = FALSE; /** * If TRUE, the created/updated fields are stored using local time. * If FALSE (the default), they are stored using UTC * @var bool */ public $local_time = FALSE; /** * If TRUE, the created/updated fields are stored as a unix timestamp, * as opposed to a formatted string. * Defaults to FALSE. * @var bool */ public $unix_timestamp = FALSE; /** * Set to a date format to override the default format of * 'Y-m-d H:i:s O' * @var string */ public $timestamp_format = ''; /** * Contains the database fields for this object. * ** Automatically configured ** * @var array */ public $fields = array(); /** * Set to a string to use when autoloading lang files. * Can contain two magic values: ${model} and ${table}. * These are automatically * replaced when looking up the language file. * Defaults to model_${model} * @var string */ public $lang_file_format = ''; /** * Set to a string to use when looking up field labels. Can contain three * magic values: ${model}, ${table}, and ${field}. These are automatically * replaced when looking up the language file. * Defaults to ${model}_${field} * @var string */ public $field_label_lang_format = ''; /** * Contains the result of the last query. * @var array */ public $all = array(); /** * Semi-private field used to track the parent model/id if there is one. * @var array */ public $parent = array(); /** * Contains the validation rules, label, and get_rules for each field. * @var array */ public $validation = array(); /** * Contains any related objects of which this model is related one or more times. * @var array */ public $has_many = array(); /** * Contains any related objects of which this model is singularly related. * @var array */ public $has_one = array(); /** * Used to enable or disable the production cache. * This should really only be set in the global configuration. * @var bool */ public $production_cache = FALSE; /** * Used to determine where to look for extensions. * This should really only be set in the global configuration. * @var string */ public $extensions_path = ''; /** * If set to an array of names, this will automatically load the * specified extensions for this model. * @var mixed */ public $extensions = NULL; /** * If a query returns more than the number of rows specified here, * then it will be automatically freed after a get. * @var int */ public $free_result_threshold = 100; /** * This can be specified as an array of fields to sort by if no other * sorting or selection has occurred. * @var mixed */ public $default_order_by = NULL; // tracks whether or not the object has already been validated protected $_validated = FALSE; // Tracks the columns that need to be instantiated after a GET protected $_instantiations = NULL; // Tracks get_rules, matches, and intval rules, to spped up _to_object protected $_field_tracking = NULL; // used to track related queries in deep relationships. protected $_query_related = array(); // If true before a related get(), any extra fields on the join table will be added. protected $_include_join_fields = FALSE; // If true before a save, this will force the next save to be new. protected $_force_save_as_new = FALSE; // If true, the next where statement will not be prefixed with an AND or OR. protected $_where_group_started = FALSE; /** * Constructors (both PHP4 and PHP5 style, to stay compatible) * * Initialize DataMapper. * @param int $id if provided, load in the object specified by that ID. */ public function __construct($id = NULL) { return $this->DataMapper($id); } public function DataMapper($id = NULL) { $this->_dmz_assign_libraries(); $this_class = strtolower(get_class($this)); $is_dmz = $this_class == 'datamapper'; if($is_dmz) { $this->_load_languages(); $this->_load_helpers(); } // this is to ensure that singular is only called once per model if(isset(DataMapper::$common[DMZ_CLASSNAMES_KEY][$this_class])) { $common_key = DataMapper::$common[DMZ_CLASSNAMES_KEY][$this_class]; } else { DataMapper::$common[DMZ_CLASSNAMES_KEY][$this_class] = $common_key = singular($this_class); } // Determine model name if (empty($this->model)) { $this->model = $common_key; } // Load stored config settings by reference foreach (DataMapper::$config as $config_key => &$config_value) { // Only if they're not already set if (empty($this->{$config_key})) { $this->{$config_key} =& $config_value; } } // Load model settings if not in common storage if ( ! isset(DataMapper::$common[$common_key])) { // If model is 'datamapper' then this is the initial autoload by CodeIgniter if ($is_dmz) { // Load config settings $this->config->load('datamapper', TRUE, TRUE); // Get and store config settings DataMapper::$config = $this->config->item('datamapper'); // now double check that all required config values were set foreach(DataMapper::$_dmz_config_defaults as $config_key => $config_value) { if(empty(DataMapper::$config[$config_key])) { DataMapper::$config[$config_key] = $config_value; } } DataMapper::_load_extensions(DataMapper::$global_extensions, DataMapper::$config['extensions']); unset(DataMapper::$config['extensions']); return; } // load language file, if requested and it exists if(!empty($this->lang_file_format)) { $lang_file = str_replace(array('${model}', '${table}'), array($this->model, $this->table), $this->lang_file_format); $deft_lang = $this->config->item('language'); $idiom = ($deft_lang == '') ? 'english' : $deft_lang; if(file_exists(APPPATH.'language/'.$idiom.'/'.$lang_file.'_lang'.EXT)) { $this->lang->load($lang_file, $idiom); } } $loaded_from_cache = FALSE; // Load in the production cache for this model, if it exists if( ! empty(DataMapper::$config['production_cache'])) { // check if it's a fully qualified path first if (!is_dir($cache_folder = DataMapper::$config['production_cache'])) { // if not, it's relative to the application path $cache_folder = APPPATH . DataMapper::$config['production_cache']; } if(file_exists($cache_folder) && is_dir($cache_folder) && is_writeable($cache_folder)) { $cache_file = $cache_folder . '/' . $common_key . EXT; if(file_exists($cache_file)) { include($cache_file); if(isset($cache)) { DataMapper::$common[$common_key] =& $cache; unset($cache); // allow subclasses to add initializations if(method_exists($this, 'post_model_init')) { $this->post_model_init(TRUE); } // Load extensions (they are not cacheable) $this->_initiate_local_extensions($common_key); $loaded_from_cache = TRUE; } } } } if(! $loaded_from_cache) { // Determine table name if (empty($this->table)) { $this->table = plural(get_class($this)); } // Add prefix to table $this->table = $this->prefix . $this->table; $this->_field_tracking = array( 'get_rules' => array(), 'matches' => array(), 'intval' => array('id') ); // Convert validation into associative array by field name $associative_validation = array(); foreach ($this->validation as $name => $validation) { if(is_string($name)) { $validation['field'] = $name; } else { $name = $validation['field']; } // clean up possibly missing fields if( ! isset($validation['rules'])) { $validation['rules'] = array(); } // Populate associative validation array $associative_validation[$name] = $validation; if (!empty($validation['get_rules'])) { $this->_field_tracking['get_rules'][] = $name; } // Check if there is a "matches" validation rule if (isset($validation['rules']['matches'])) { $this->_field_tracking['matches'][$name] = $validation['rules']['matches']; } } // set up id column, if not set if(!isset($associative_validation['id'])) { // label is set below, to prevent caching language-based labels $associative_validation['id'] = array( 'field' => 'id', 'rules' => array('integer') ); } $this->validation = $associative_validation; // Force all other has_one ITFKs to integers on get foreach($this->has_one as $related => $rel_props) { $field = $related . '_id'; if( in_array($field, $this->fields) && ( ! isset($this->validation[$field]) || // does not have a validation key or... ! isset($this->validation[$field]['get_rules'])) && // a get_rules key... ( ! isset($this->validation[$related]) || // nor does the related have a validation key or... ! isset($this->validation[$related]['get_rules'])) ) // a get_rules key { // assume an int $this->_field_tracking['intval'][] = $field; } } // Get and store the table's field names and meta data $fields = $this->db->field_data($this->table); // Store only the field names and ensure validation list includes all fields foreach ($fields as $field) { // Populate fields array $this->fields[] = $field->name; // Add validation if current field has none if ( ! isset($this->validation[$field->name])) { // label is set below, to prevent caching language-based labels $this->validation[$field->name] = array('field' => $field->name, 'rules' => array()); } } // convert simple has_one and has_many arrays into more advanced ones foreach(array('has_one', 'has_many') as $arr) { foreach ($this->{$arr} as $related_field => $rel_props) { // process the relationship $this->_relationship($arr, $rel_props, $related_field); } } // allow subclasses to add initializations if(method_exists($this, 'post_model_init')) { $this->post_model_init(FALSE); } // Store common model settings foreach (array('table', 'fields', 'validation', 'has_one', 'has_many', '_field_tracking') as $item) { DataMapper::$common[$common_key][$item] = $this->{$item}; } // store the item to the production cache $this->production_cache(); // Load extensions last, so they aren't cached. $this->_initiate_local_extensions($common_key); } // Finally, localize the labels here (because they shouldn't be cached // This also sets any missing labels. $validation =& DataMapper::$common[$common_key]['validation']; foreach($validation as $field => &$val) { // Localize label if necessary $val['label'] = $this->localize_label($field, isset($val['label']) ? $val['label'] : FALSE); } unset($validation); } // Load stored common model settings by reference foreach(DataMapper::$common[$common_key] as $key => &$value) { $this->{$key} =& $value; } // Clear object properties to set at default values $this->clear(); if( ! empty($id) && is_numeric($id)) { $this->get_by_id(intval($id)); } } // -------------------------------------------------------------------- /** * Reloads in the configuration data for a model. This is mainly * used to handle language changes. Only this instance and new instances * will see the changes. */ public function reinitialize_model() { // this is to ensure that singular is only called once per model if(isset(DataMapper::$common[DMZ_CLASSNAMES_KEY][$this_class])) { $common_key = DataMapper::$common[DMZ_CLASSNAMES_KEY][$this_class]; } else { DataMapper::$common[DMZ_CLASSNAMES_KEY][$this_class] = $common_key = singular($this_class); } unset(DataMapper::$common[$common_key]); $model = get_class($this); new $model(); // re-initialze // Load stored common model settings by reference foreach(DataMapper::$common[$common_key] as $key => &$value) { $this->{$key} =& $value; } } // -------------------------------------------------------------------- /** * Autoload * * Autoloads object classes that are used with DataMapper. * This method will look in any model directories available to CI. * * Note: * It is important that they are autoloaded as loading them manually with * CodeIgniter's loader class will cause DataMapper's __get and __set functions * to not function. * * @param string $class Name of class to load. */ public static function autoload($class) { $CI =& get_instance(); // Don't attempt to autoload CI_ , EE_, or custom prefixed classes if (in_array(substr($class, 0, 3), array('CI_', 'EE_')) OR strpos($class, $CI->config->item('subclass_prefix')) === 0) { return; } // Prepare class $class = strtolower($class); // Prepare path if (isset($CI->load->_ci_model_paths) && is_array($CI->load->_ci_model_paths)) { // use CI 2.0 loader's model paths $paths = $CI->load->_ci_model_paths; } else { // search only the applications models folder $paths[] = APPPATH; } foreach ($paths as $path) { // Prepare file $file = $path . 'models/' . $class . EXT; // Check if file exists, require_once if it does if (file_exists($file)) { require_once($file); break; } } // if class not loaded, do a recursive search of model paths for the class if (! class_exists($class)) { foreach($paths as $path) { $found = DataMapper::recursive_require_once($class, $path . 'models'); if($found) { break; } } } } // -------------------------------------------------------------------- /** * Recursive Require Once * * Recursively searches the path for the class, require_once if found. * * @param string $class Name of class to look for * @param string $path Current path to search */ protected static function recursive_require_once($class, $path) { $found = FALSE; if(is_dir($path)) { $handle = opendir($path); if ($handle) { while (FALSE !== ($dir = readdir($handle))) { // If dir does not contain a dot if (strpos($dir, '.') === FALSE) { // Prepare recursive path $recursive_path = $path . '/' . $dir; // Prepare file $file = $recursive_path . '/' . $class . EXT; // Check if file exists, require_once if it does if (file_exists($file)) { require_once($file); $found = TRUE; break; } else if (is_dir($recursive_path)) { // Do a recursive search of the path for the class DataMapper::recursive_require_once($class, $recursive_path); } } } closedir($handle); } } return $found; } // -------------------------------------------------------------------- /** * Loads in any extensions used by this class or globally. * * @param array $extensions List of extensions to add to. * @param array $name List of new extensions to load. */ protected static function _load_extensions(&$extensions, $names) { $CI =& get_instance(); $class_prefixes = array( 0 => 'DMZ_', 1 => 'DataMapper_', 2 => $CI->config->item('subclass_prefix'), 3 => 'CI_' ); foreach($names as $name => $options) { if( ! is_string($name)) { $name = $options; $options = NULL; } // only load an extension if it wasn't already loaded in this context if(isset($extensions[$name])) { return; } if( ! isset($extensions['_methods'])) { $extensions['_methods'] = array(); } // determine the file name and class name if(strpos($name, '/') === FALSE) { $file = APPPATH . DataMapper::$config['extensions_path'] . '/' . $name . EXT; $ext = $name; } else { $file = APPPATH . $name . EXT; $ext = array_pop(explode('/', $name)); } if(!file_exists($file)) { show_error('DataMapper Error: loading extension ' . $name . ': File not found.'); } // load class include_once($file); // Allow for DMZ_Extension, DataMapper_Extension, etc. foreach($class_prefixes as $index => $prefix) { if(class_exists($prefix.$ext)) { if($index == 2) // "MY_" { // Load in the library this class is based on $CI->load->library($ext); } $ext = $prefix.$ext; break; } } if(!class_exists($ext)) { show_error("DataMapper Error: Unable to find a class for extension $name."); } // create class if(is_null($options)) { $o = new $ext(); } else { $o = new $ext($options); } $extensions[$name] = $o; // figure out which methods can be called on this class. $methods = get_class_methods($ext); foreach($methods as $m) { // do not load private methods or methods already loaded. if($m[0] !== '_' && is_callable(array($o, $m)) && ! isset($extensions['_methods'][$m]) ) { // store this method. $extensions['_methods'][$m] = $name; } } } } // -------------------------------------------------------------------- /** * Loads the extensions that are local to this model. * @param string $common_key Shared key to save extenions to. */ private function _initiate_local_extensions($common_key) { if(!empty($this->extensions)) { $extensions = $this->extensions; $this->extensions = array(); DataMapper::_load_extensions($this->extensions, $extensions); } else { // ensure an empty array $this->extensions = array('_methods' => array()); } // bind to the shared key, for dynamic loading DataMapper::$common[$common_key]['extensions'] =& $this->extensions; } // -------------------------------------------------------------------- /** * Dynamically load an extension when needed. * @param object $name Name of the extension (or array of extensions). * @param array $options Options for the extension * @param boolean $local If TRUE, only loads the extension into this object */ public function load_extension($name, $options = NULL, $local = FALSE) { if( ! is_array($name)) { if( ! is_null($options)) { $name = array($name => $options); } else { $name = array($name); } } // called individually to ensure that the array is modified directly // (and not copied instead) if($local) { DataMapper::_load_extensions($this->extensions, $name); } else { DataMapper::_load_extensions(DataMapper::$global_extensions, $name); } } // -------------------------------------------------------------------- /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Magic methods * * * * The following are methods to override the default PHP behaviour. * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ // -------------------------------------------------------------------- /** * Magic Get * * Returns the value of the named property. * If named property is a related item, instantiate it first. * * This method also instantiates the DB object and the form_validation * objects as necessary * * @ignore * @param string $name Name of property to look for * @return mixed */ public function __get($name) { // We dynamically get DB when needed, and create a copy. // This allows multiple queries to be generated at the same time. if($name == 'db') { $CI =& get_instance(); if($this->db_params === FALSE) { $this->db =& $CI->db; } else { if($this->db_params == '' || $this->db_params === TRUE) { // ensure the shared DB is disconnected, even if the app exits uncleanly if(!isset($CI->db->_has_shutdown_hook)) { register_shutdown_function(array($CI->db, 'close')); $CI->db->_has_shutdown_hook = TRUE; } // clone, so we don't create additional connections to the DB $this->db = clone($CI->db); $this->db->_reset_select(); } else { // connecting to a different database, so we *must* create additional copies. // It is up to the developer to close the connection! $this->db = $CI->load->database($this->db_params, TRUE, TRUE); } // these items are shared (for debugging) if(isset($CI->db)) { $this->db->queries =& $CI->db->queries; $this->db->query_times =& $CI->db->query_times; } } // ensure the created DB is disconnected, even if the app exits uncleanly if(!isset($this->db->_has_shutdown_hook)) { register_shutdown_function(array($this->db, 'close')); $this->db->_has_shutdown_hook = TRUE; } return $this->db; } // Special case to get form_validation when first accessed if($name == 'form_validation') { if ( ! isset($this->form_validation) ) { $CI =& get_instance(); if( ! isset($CI->form_validation)) { $CI->load->library('form_validation'); $this->lang->load('form_validation'); unset($CI->load->_ci_classes['form_validation']); } $this->form_validation = $CI->form_validation; } return $this->form_validation; } $has_many = isset($this->has_many[$name]); $has_one = isset($this->has_one[$name]); // If named property is a "has many" or "has one" related item if ($has_many || $has_one) { $related_properties = $has_many ? $this->has_many[$name] : $this->has_one[$name]; // Instantiate it before accessing $class = $related_properties['class']; $this->{$name} = new $class(); // Store parent data $this->{$name}->parent = array('model' => $related_properties['other_field'], 'id' => $this->id); // Check if Auto Populate for "has many" or "has one" is on // (but only if this object exists in the DB, and we aren't instantiating) if ($this->exists() && ($has_many && $this->auto_populate_has_many) || ($has_one && $this->auto_populate_has_one)) { $this->{$name}->get(); } return $this->{$name}; } $name_single = singular($name); if($name_single !== $name) { // possibly return single form of name $test = $this->{$name_single}; if(is_object($test)) { return $test; } } return NULL; } // -------------------------------------------------------------------- /** * Used several places to temporarily override the auto_populate setting * @ignore * @param string $related Related Name * @return DataMapper|NULL */ private function &_get_without_auto_populating($related) { $b_many = $this->auto_populate_has_many; $b_one = $this->auto_populate_has_one; $this->auto_populate_has_many = FALSE; $this->auto_populate_has_one = FALSE; $ret =& $this->{$related}; $this->auto_populate_has_many = $b_many; $this->auto_populate_has_one = $b_one; return $ret; } // -------------------------------------------------------------------- /** * Magic Call * * Calls special methods, or extension methods. * * @ignore * @param string $method Method name * @param array $arguments Arguments to method * @return mixed */ public function __call($method, $arguments) { // List of watched method names // NOTE: order matters: make sure more specific items are listed before // less specific items $watched_methods = array( 'save_', 'delete_', 'get_by_related_', 'get_by_related', 'get_by_', '_related_subquery', '_subquery', '_related_', '_related', '_join_field', '_field_func', '_func' ); foreach ($watched_methods as $watched_method) { // See if called method is a watched method if (strpos($method, $watched_method) !== FALSE) { $pieces = explode($watched_method, $method); if ( ! empty($pieces[0]) && ! empty($pieces[1])) { // Watched method is in the middle return $this->{'_' . trim($watched_method, '_')}($pieces[0], array_merge(array($pieces[1]), $arguments)); } else { // Watched method is a prefix or suffix return $this->{'_' . trim($watched_method, '_')}(str_replace($watched_method, '', $method), $arguments); } } } // attempt to call an extension $ext = NULL; if($this->_extension_method_exists($method, 'local')) { $name = $this->extensions['_methods'][$method]; $ext = $this->extensions[$name]; } else if($this->_extension_method_exists($method, 'global')) { $name = DataMapper::$global_extensions['_methods'][$method]; $ext = DataMapper::$global_extensions[$name]; } if( ! is_null($ext)) { array_unshift($arguments, $this); return call_user_func_array(array($ext, $method), $arguments); } // show an error, for debugging's sake. throw new Exception("Unable to call the method \"$method\" on the class " . get_class($this)); } // -------------------------------------------------------------------- /** * Returns TRUE or FALSE if the method exists in the extensions. * * @param object $method Method to look for. * @param object $which One of 'both', 'local', or 'global' * @return bool TRUE if the method can be called. */ private function _extension_method_exists($method, $which = 'both') { $found = FALSE; if($which != 'global') { $found = ! empty($this->extensions) && isset($this->extensions['_methods'][$method]); } if( ! $found && $which != 'local' ) { $found = ! empty(DataMapper::$global_extensions) && isset(DataMapper::$global_extensions['_methods'][$method]); } return $found; } // -------------------------------------------------------------------- /** * Magic Clone * * Allows for a less shallow clone than the default PHP clone. * * @ignore */ public function __clone() { foreach ($this as $key => $value) { if (is_object($value) && $key != 'db') { $this->{$key} = clone($value); } } } // -------------------------------------------------------------------- /** * To String * * Converts the current object into a string. * Should be overridden by extended objects. * * @return string */ public function __toString() { return ucfirst($this->model); } // -------------------------------------------------------------------- /** * Allows the all array to be iterated over without * having to specify it. * * @return Iterator An iterator for the all array */ public function getIterator() { if(isset($this->_dm_dataset_iterator)) { return $this->_dm_dataset_iterator; } else { return new ArrayIterator($this->all); } } // -------------------------------------------------------------------- /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Main methods * * * * The following are methods that form the main * * functionality of DataMapper. * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ // -------------------------------------------------------------------- /** * Get * * Get objects from the database. * * @param integer|NULL $limit Limit the number of results. * @param integer|NULL $offset Offset the results when limiting. * @return DataMapper Returns self for method chaining. */ public function get($limit = NULL, $offset = NULL) { // Check if this is a related object and if so, perform a related get if (! $this->_handle_related()) { // invalid get request, return this for chaining. return $this; } // Else fall through to a normal get $query = FALSE; // Check if object has been validated (skipped for related items) if ($this->_validated && empty($this->parent)) { // Reset validated $this->_validated = FALSE; // Use this objects properties $data = $this->_to_array(TRUE); if ( ! empty($data)) { // Clear this object to make way for new data $this->clear(); // Set up default order by (if available) $this->_handle_default_order_by(); // Get by objects properties $query = $this->db->get_where($this->table, $data, $limit, $offset); } // FIXME: notify user if nothing was set? } else { // Clear this object to make way for new data $this->clear(); // Set up default order by (if available) $this->_handle_default_order_by(); // Get by built up query $query = $this->db->get($this->table, $limit, $offset); } // Convert the query result into DataMapper objects if($query) { $this->_process_query($query); } // For method chaining return $this; } // -------------------------------------------------------------------- /** * Returns the SQL string of the current query (SELECTs ONLY). * NOTE: This also _clears_ the current query info. * * This can be used to generate subqueries. * * @param integer|NULL $limit Limit the number of results. * @param integer|NULL $offset Offset the results when limiting. * @return string SQL as a string. */ public function get_sql($limit = NULL, $offset = NULL, $handle_related = FALSE) { if($handle_related) { $this->_handle_related(); } $this->db->_track_aliases($this->table); $this->db->from($this->table); $this->_handle_default_order_by(); if ( ! is_null($limit)) { $this->limit($limit, $offset); } $sql = $this->db->_compile_select(); $this->_clear_after_query(); return $sql; } // -------------------------------------------------------------------- /** * Runs the query, but returns the raw CodeIgniter results * NOTE: This also _clears_ the current query info. * * @param integer|NULL $limit Limit the number of results. * @param integer|NULL $offset Offset the results when limiting. * @return CI_DB_result Result Object */ public function get_raw($limit = NULL, $offset = NULL, $handle_related = TRUE) { if($handle_related) { $this->_handle_related(); } $this->_handle_default_order_by(); $query = $this->db->get($this->table, $limit, $offset); $this->_clear_after_query(); return $query; } // -------------------------------------------------------------------- /** * Returns a streamable result set for large queries. * Usage: * $rs = $object->get_iterated(); * $size = $rs->count; * foreach($rs as $o) { * // handle $o * } * $rs can be looped through more than once. * * @param integer|NULL $limit Limit the number of results. * @param integer|NULL $offset Offset the results when limiting. * @return DataMapper Returns self for method chaining. */ public function get_iterated($limit = NULL, $offset = NULL) { // clone $this, so we keep track of instantiations, etc. // because these are cleared after the call to get_raw $object = $this->get_clone(); // need to clear query from the clone $object->db->_reset_select(); // Clear the query related list from the clone $object->_query_related = array(); // Build iterator $this->_dm_dataset_iterator = new DM_DatasetIterator($object, $this->get_raw($limit, $offset, TRUE)); return $this; } // -------------------------------------------------------------------- /** * Convenience method that runs a query based on pages. * This object will have two new values, $query_total_pages and * $query_total_rows, which can be used to determine how many pages and * how many rows are available in total, respectively. * * @param int $page Page (1-based) to start on, or row (0-based) to start on * @param int $page_size Number of rows in a page * @param bool $page_num_by_rows When TRUE, $page is the starting row, not the starting page * @param bool $iterated Internal Use Only * @return DataMapper Returns self for method chaining. */ public function get_paged($page = 1, $page_size = 50, $page_num_by_rows = FALSE, $info_object = 'paged', $iterated = FALSE) { // first, duplicate this query, so we have a copy for the query $count_query = $this->get_clone(TRUE); if($page_num_by_rows) { $page = 1 + floor(intval($page) / $page_size); } // never less than 1 $page = max(1, intval($page)); $offset = $page_size * ($page - 1); // for performance, we clear out the select AND the order by statements, // since they aren't necessary and might slow down the query. $count_query->db->ar_select = NULL; $count_query->db->ar_orderby = NULL; $total = $count_query->db->ar_distinct ? $count_query->count_distinct() : $count_query->count(); // common vars $last_row = $page_size * floor($total / $page_size); $total_pages = ceil($total / $page_size); if($offset >= $last_row) { // too far! $offset = $last_row; $page = $total_pages; } // now query this object if($iterated) { $this->get_iterated($page_size, $offset); } else { $this->get($page_size, $offset); } $this->{$info_object} = new stdClass(); $this->{$info_object}->page_size = $page_size; $this->{$info_object}->items_on_page = $this->result_count(); $this->{$info_object}->current_page = $page; $this->{$info_object}->current_row = $offset; $this->{$info_object}->total_rows = $total; $this->{$info_object}->last_row = $last_row; $this->{$info_object}->total_pages = $total_pages; $this->{$info_object}->has_previous = $offset > 0; $this->{$info_object}->previous_page = max(1, $page-1); $this->{$info_object}->previous_row = max(0, $offset-$page_size); $this->{$info_object}->has_next = $page < $total_pages; $this->{$info_object}->next_page = min($total_pages, $page+1); $this->{$info_object}->next_row = min($last_row, $offset+$page_size); return $this; } // -------------------------------------------------------------------- /** * Runs get_paged, but as an Iterable. * * @see get_paged * @param int $page Page (1-based) to start on, or row (0-based) to start on * @param int $page_size Number of rows in a page * @param bool $page_num_by_rows When TRUE, $page is the starting row, not the starting page * @param bool $iterated Internal Use Only * @return DataMapper Returns self for method chaining. */ public function get_paged_iterated($page = 1, $page_size = 50, $page_num_by_rows = FALSE, $info_object = 'paged') { return $this->get_paged($page, $page_size, $page_num_by_rows, $info_object, TRUE); } // -------------------------------------------------------------------- /** * Forces this object to be INSERTed, even if it has an ID. * * @param mixed $object See save. * @param string $related_field See save. * @return bool Result of the save. */ public function save_as_new($object = '', $related_field = '') { $this->_force_save_as_new = TRUE; return $this->save($object, $related_field); } // -------------------------------------------------------------------- /** * Save * * Saves the current record, if it validates. * If object is supplied, saves relations between this object and the supplied object(s). * * @param mixed $object Optional object to save or array of objects to save. * @param string $related_field Optional string to save the object as a specific relationship. * @return bool Success or Failure of the validation and save. */ 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(); 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)); } // -------------------------------------------------------------------- /** * Recursively saves arrays of objects if they are In-Table Foreign Keys. * @ignore * @param object $objects Objects to save. This array may be modified. * @param object $related_field Related Field name (empty is OK) */ protected function _save_itfk( &$objects, $related_field) { foreach($objects as $index => $o) { if(is_int($index)) { $rf = $related_field; } else { $rf = $index; } if(is_array($o)) { $this->_save_itfk($o, $rf); if(empty($o)) { unset($objects[$index]); } } else { if(empty($rf)) { $rf = $o->model; } $related_properties = $this->_get_related_properties($rf); $other_column = $related_properties['join_other_as'] . '_id'; if(isset($this->has_one[$rf]) && in_array($other_column, $this->fields)) { // unset, so that it doesn't get re-saved later. unset($objects[$index]); if($this->{$other_column} != $o->id) { // ITFK: store on the table $this->{$other_column} = $o->id; // Remove reverse relationships for one-to-ones $this->_remove_other_one_to_one($rf, $o); } } } } } // -------------------------------------------------------------------- /** * Recursively saves arrays of objects. * * @ignore * @param object $object Array of objects to save, or single object * @param object $related_field Default related field name (empty is OK) * @return bool TRUE or FALSE if an error occurred. */ protected function _save_related_recursive($object, $related_field) { if(is_array($object)) { $success = TRUE; foreach($object as $rk => $o) { if(is_int($rk)) { $rk = $related_field; } $rec_success = $this->_save_related_recursive($o, $rk); $success = $success && $rec_success; } return $success; } else { return $this->_save_relation($object, $related_field); } } // -------------------------------------------------------------------- /** * _Save * * Used by __call to process related saves. * * @ignore * @param mixed $related_field * @param array $arguments * @return bool */ private function _save($related_field, $arguments) { return $this->save($arguments[0], $related_field); } // -------------------------------------------------------------------- /** * Update * * Allows updating of more than one row at once. * * @param object $field A field to update, or an array of fields => values * @param object $value The new value * @param object $escape_values If false, don't escape the values * @return bool TRUE or FALSE on success or failure */ public function update($field, $value = NULL, $escape_values = TRUE) { if( ! is_array($field)) { $field = array($field => $value); } else if($value === FALSE) { $escape_values = FALSE; } if(empty($field)) { show_error("Nothing was provided to update."); } // Check if object has an 'updated' field if (in_array($this->updated_field, $this->fields)) { $timestamp = $this->_get_generated_timestamp(); if( ! $escape_values) { $timestamp = $this->db->escape($timestamp); } // Update updated datetime $field[$this->updated_field] = $timestamp; } foreach($field as $k => $v) { if( ! $escape_values) { // attempt to add the table name $v = $this->add_table_name($v); } $this->db->set($k, $v, $escape_values); } return $this->db->update($this->table); } // -------------------------------------------------------------------- /** * Update All * * Updates all items that are in the all array. * * @param object $field A field to update, or an array of fields => values * @param object $value The new value * @param object $escape_values If false, don't escape the values * @return bool TRUE or FALSE on success or failure */ public function update_all($field, $value = NULL, $escape_values = TRUE) { $ids = array(); foreach($this->all as $object) { $ids[] = $object->id; } if(empty($ids)) { return FALSE; } $this->where_in('id', $ids); return $this->update($field, $value, $escape_values); } // -------------------------------------------------------------------- /** * Gets a timestamp to use when saving. * @return mixed */ 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; } // -------------------------------------------------------------------- /** * Delete * * Deletes the current record. * If object is supplied, deletes relations between this object and the supplied object(s). * * @param mixed $object If specified, delete the relationship to the object or array of objects. * @param string $related_field Can be used to specify which relationship to delete. * @return bool Success or Failure of the delete. */ public function delete($object = '', $related_field = '') { if (empty($object) && ! is_array($object)) { if ( ! empty($this->id)) { // Begin auto transaction $this->_auto_trans_begin(); // Delete all "has many" and "has one" relations for this object first foreach (array('has_many', 'has_one') as $type) { foreach ($this->{$type} as $model => $properties) { // Prepare model $class = $properties['class']; $object = new $class(); $this_model = $properties['join_self_as']; $other_model = $properties['join_other_as']; // Determine relationship table name $relationship_table = $this->_get_relationship_table($object, $model); // We have to just set NULL for in-table foreign keys that // are pointing at this object if($relationship_table == $object->table && // ITFK // NOT ITFKs that point at the other object ! ($object->table == $this->table && // self-referencing has_one join in_array($other_model . '_id', $this->fields)) // where the ITFK is for the other object ) { $data = array($this_model . '_id' => NULL); // Update table to remove relationships $this->db->where($this_model . '_id', $this->id); $this->db->update($object->table, $data); } else if ($relationship_table != $this->table) { $data = array($this_model . '_id' => $this->id); // Delete relation $this->db->delete($relationship_table, $data); } // Else, no reason to delete the relationships on this table } } // Delete the object itself $this->db->where('id', $this->id); $this->db->delete($this->table); // Complete auto transaction $this->_auto_trans_complete('delete'); // Clear this object $this->clear(); return TRUE; } } else if (is_array($object)) { // Begin auto transaction $this->_auto_trans_begin(); // Temporarily store the success/failure $result = array(); foreach ($object as $rel_field => $obj) { if (is_int($rel_field)) { $rel_field = $related_field; } if (is_array($obj)) { foreach ($obj as $r_f => $o) { if (is_int($r_f)) { $r_f = $rel_field; } $result[] = $this->_delete_relation($o, $r_f); } } else { $result[] = $this->_delete_relation($obj, $rel_field); } } // Complete auto transaction $this->_auto_trans_complete('delete (relationship)'); // If no failure was recorded, return TRUE if ( ! in_array(FALSE, $result)) { return TRUE; } } else { // Begin auto transaction $this->_auto_trans_begin(); // Temporarily store the success/failure $result = $this->_delete_relation($object, $related_field); // Complete auto transaction $this->_auto_trans_complete('delete (relationship)'); return $result; } return FALSE; } // -------------------------------------------------------------------- /** * _Delete * * Used by __call to process related deletes. * * @ignore * @param string $related_field * @param array $arguments * @return bool */ private function _delete($related_field, $arguments) { return $this->delete($arguments[0], $related_field); } // -------------------------------------------------------------------- /** * Delete All * * Deletes all records in this objects all list. * * @return bool Success or Failure of the delete */ public function delete_all() { $success = TRUE; foreach($this as $item) { if ( ! empty($item->id)) { $success_temp = $item->delete(); $success = $success && $success_temp; } } $this->clear(); return $success; } // -------------------------------------------------------------------- /** * Refresh All * * Removes any empty objects in this objects all list. * Only needs to be used if you are looping through the all list * a second time and you have deleted a record the first time through. * * @return bool FALSE if the $all array was already empty. */ public function refresh_all() { if ( ! empty($this->all)) { $all = array(); foreach ($this->all as $item) { if ( ! empty($item->id)) { $all[] = $item; } } $this->all = $all; return TRUE; } return FALSE; } // -------------------------------------------------------------------- /** * Validate * * Validates the value of each property against the assigned validation rules. * * @param mixed $object Objects included with the validation [from save()]. * @param string $related_field See save. * @return DataMapper Returns $this for method chanining. */ public function validate($object = '', $related_field = '') { // Return if validation has already been run if ($this->_validated) { // For method chaining return $this; } // Set validated as having been run $this->_validated = TRUE; // Clear errors $this->error = new DM_Error_Object(); // Loop through each property to be validated foreach ($this->validation as $field => $validation) { if(empty($validation['rules'])) { continue; } // Get validation settings $rules = $validation['rules']; // Will validate differently if this is for a related item $related = (isset($this->has_many[$field]) || isset($this->has_one[$field])); // Check if property has changed since validate last ran if ($related || ! isset($this->stored->{$field}) || $this->{$field} !== $this->stored->{$field}) { // Only validate if field is related or required or has a value if ( ! $related && ! in_array('required', $rules) && ! in_array('always_validate', $rules)) { if ( ! isset($this->{$field}) || $this->{$field} === '') { continue; } } $label = ( ! empty($validation['label'])) ? $validation['label'] : $field; // Loop through each rule to validate this property against foreach ($rules as $rule => $param) { // Check for parameter if (is_numeric($rule)) { $rule = $param; $param = ''; } // Clear result $result = ''; // Clear message $line = FALSE; // Check rule exists if ($related) { // Prepare rule to use different language file lines $rule = 'related_' . $rule; $arg = $object; if( ! empty($related_field)) { $arg = array($related_field => $object); } if (method_exists($this, '_' . $rule)) { // Run related rule from DataMapper or the class extending DataMapper $line = $result = $this->{'_' . $rule}($arg, $field, $param); } else if($this->_extension_method_exists('rule_' . $rule)) { $line = $result = $this->{'rule_' . $rule}($arg, $field, $param); } } else if (method_exists($this, '_' . $rule)) { // Run rule from DataMapper or the class extending DataMapper $line = $result = $this->{'_' . $rule}($field, $param); } else if($this->_extension_method_exists('rule_' . $rule)) { // Run an extension-based rule. $line = $result = $this->{'rule_' . $rule}($field, $param); } else if (method_exists($this->form_validation, $rule)) { // Run rule from CI Form Validation $result = $this->form_validation->{$rule}($this->{$field}, $param); } else if (function_exists($rule)) { // Run rule from PHP $this->{$field} = $rule($this->{$field}); } // Add an error message if the rule returned FALSE if (is_string($line) || $result === FALSE) { if(!is_string($line)) { if (FALSE === ($line = $this->lang->line($rule))) { // Get corresponding error from language file $line = 'Unable to access an error message corresponding to your rule name: '.$rule.'.'; } } // Check if param is an array if (is_array($param)) { // Convert into a string so it can be used in the error message $param = implode(', ', $param); // Replace last ", " with " or " if (FALSE !== ($pos = strrpos($param, ', '))) { $param = substr_replace($param, ' or ', $pos, 2); } } // Check if param is a validation field if (isset($this->validation[$param])) { // Change it to the label value $param = $this->validation[$param]['label']; } // Add error message $this->error_message($field, sprintf($line, $label, $param)); // Escape to prevent further error checks break; } } } } // Set whether validation passed $this->valid = empty($this->error->all); // For method chaining return $this; } // -------------------------------------------------------------------- /** * Skips validation for the next call to save. * Note that this also prevents the validation routine from running until the next get. * * @param object $skip If FALSE, re-enables validation. * @return DataMapper Returns self for method chaining. */ public function skip_validation($skip = TRUE) { $this->_validated = $skip; $this->valid = $skip; return $this; } // -------------------------------------------------------------------- /** * Clear * * Clears the current object. */ public function clear() { // Clear the all list $this->all = array(); // Clear errors $this->error = new DM_Error_Object(); // Clear this objects properties and set blank error messages in case they are accessed foreach ($this->fields as $field) { $this->{$field} = NULL; } // Clear this objects "has many" related objects foreach ($this->has_many as $related => $properties) { unset($this->{$related}); } // Clear this objects "has one" related objects foreach ($this->has_one as $related => $properties) { unset($this->{$related}); } // Clear the query related list $this->_query_related = array(); // Clear and refresh stored values $this->stored = new stdClass(); // Clear the saved iterator unset($this->_dm_dataset_iterator); $this->_refresh_stored_values(); } // -------------------------------------------------------------------- /** * Clears the db object after processing a query, or returning the * SQL for a query. * * @ignore */ protected function _clear_after_query() { // clear the query as if it was run $this->db->_reset_select(); // in case some include_related instantiations were set up, clear them $this->_instantiations = NULL; // Clear the query related list (Thanks to TheJim) $this->_query_related = array(); // Clear the saved iterator unset($this->_dm_dataset_iterator); } // -------------------------------------------------------------------- /** * Count * * Returns the total count of the object records from the database. * If on a related object, returns the total count of related objects records. * * @param array $exclude_ids A list of ids to exlcude from the count * @return int Number of rows in query. */ public function count($exclude_ids = NULL, $column = NULL, $related_id = NULL) { // Check if related object if ( ! empty($this->parent)) { // Prepare model $related_field = $this->parent['model']; $related_properties = $this->_get_related_properties($related_field); $class = $related_properties['class']; $other_model = $related_properties['join_other_as']; $this_model = $related_properties['join_self_as']; $object = new $class(); // Determine relationship table name $relationship_table = $this->_get_relationship_table($object, $related_field); // To ensure result integrity, group all previous queries if( ! empty($this->db->ar_where)) { // if the relationship table is different from our table, include our table in the count query if ($relationship_table != $this->table) { $this->db->join($this->table, $this->table . '.id = ' . $relationship_table . '.' . $this_model.'_id', 'LEFT OUTER'); } array_unshift($this->db->ar_where, '( '); $this->db->ar_where[] = ' )'; } // We have to query special for in-table foreign keys that // are pointing at this object if($relationship_table == $object->table && // ITFK // NOT ITFKs that point at the other object ! ($object->table == $this->table && // self-referencing has_one join in_array($other_model . '_id', $this->fields)) // where the ITFK is for the other object ) { // ITFK on the other object's table $this->db->where('id', $this->parent['id'])->where($this_model . '_id IS NOT NULL'); } else { // All other cases $this->db->where($relationship_table . '.' . $other_model . '_id', $this->parent['id']); } if(!empty($exclude_ids)) { $this->db->where_not_in($relationship_table . '.' . $this_model . '_id', $exclude_ids); } if($column == 'id') { $column = $relationship_table . '.' . $this_model . '_id'; } if(!empty($related_id)) { $this->db->where($this_model . '_id', $related_id); } $this->db->from($relationship_table); } else { $this->db->from($this->table); if(!empty($exclude_ids)) { $this->db->where_not_in('id', $exclude_ids); } if(!empty($related_id)) { $this->db->where('id', $related_id); } $column = $this->add_table_name($column); } // Manually overridden to allow for COUNT(DISTINCT COLUMN) $select = $this->db->_count_string; if(!empty($column)) { // COUNT DISTINCT $select = 'SELECT COUNT(DISTINCT ' . $this->db->_protect_identifiers($column) . ') AS '; } $sql = $this->db->_compile_select($select . $this->db->_protect_identifiers('numrows')); $query = $this->db->query($sql); $this->db->_reset_select(); if ($query->num_rows() == 0) { return 0; } $row = $query->row(); return intval($row->numrows); } // -------------------------------------------------------------------- /** * Count Distinct * * Returns the total count of distinct object records from the database. * If on a related object, returns the total count of related objects records. * * @param array $exclude_ids A list of ids to exlcude from the count * @param string $column If provided, use this column for the DISTINCT instead of 'id' * @return int Number of rows in query. */ public function count_distinct($exclude_ids = NULL, $column = 'id') { return $this->count($exclude_ids, $column); } // -------------------------------------------------------------------- /** * Convenience method to return the number of items from * the last call to get. * * @return int */ public function result_count() { if(isset($this->_dm_dataset_iterator)) { return $this->_dm_dataset_iterator->result_count(); } else { return count($this->all); } } // -------------------------------------------------------------------- /** * Exists * * Returns TRUE if the current object has a database record. * * @return bool */ public function exists() { // returns TRUE if the id of this object is set and not empty, OR // there are items in the ALL array. return isset($this->id) ? !empty($this->id) : ($this->result_count() > 0); } // -------------------------------------------------------------------- /** * Query * * Runs the specified query and populates the current object with the results. * * Warning: Use at your own risk. This will only be as reliable as your query. * * @param string $sql The query to process * @param array|bool $binds Array of values to bind (see CodeIgniter) * @return DataMapper Returns self for method chaining. */ public function query($sql, $binds = FALSE) { // Get by objects properties $query = $this->db->query($sql, $binds); $this->_process_query($query); // For method chaining return $this; } // -------------------------------------------------------------------- /** * Check Last Query * Renders the last DB query performed. * * @param array $delims Delimiters for the SQL string. * @param bool $return_as_string If TRUE, don't output automatically. * @return string Last db query formatted as a string. */ public function check_last_query($delims = array('
', '
'), $return_as_string = FALSE) { $q = wordwrap($this->db->last_query(), 100, "\n\t"); if(!empty($delims)) { $q = implode($q, $delims); } if($return_as_string === FALSE) { echo $q; } return $q; } // -------------------------------------------------------------------- /** * Error Message * * Adds an error message to this objects error object. * * @param string $field Field to set the error on. * @param string $error Error message. */ public function error_message($field, $error) { if ( ! empty($field) && ! empty($error)) { // Set field specific error $this->error->{$field} = $this->error_prefix . $error . $this->error_suffix; // Add field error to errors all list $this->error->all[$field] = $this->error->{$field}; // Append field error to error message string $this->error->string .= $this->error->{$field}; } } // -------------------------------------------------------------------- /** * Get Clone * * Returns a clone of the current object. * * @return DataMapper Cloned copy of this object. */ public function get_clone($force_db = FALSE) { $temp = clone($this); // This must be left in place, even with the __clone method, // or else the DB will not be copied over correctly. if($force_db || (($this->db_params !== FALSE) && isset($this->db)) ) { // create a copy of $this->db $temp->db = clone($this->db); } return $temp; } // -------------------------------------------------------------------- /** * Get Copy * * Returns an unsaved copy of the current object. * * @return DataMapper Cloned copy of this object with an empty ID for saving as new. */ public function get_copy($force_db = FALSE) { $copy = $this->get_clone($force_db); $copy->id = NULL; return $copy; } // -------------------------------------------------------------------- /** * Get By * * Gets objects by specified field name and value. * * @ignore * @param string $field Field to look at. * @param array $value Arguments to this method. * @return DataMapper Returns self for method chaining. */ private function _get_by($field, $value = array()) { if (isset($value[0])) { $this->where($field, $value[0]); } return $this->get(); } // -------------------------------------------------------------------- /** * Get By Related * * Gets objects by specified related object and optionally by field name and value. * * @ignore * @param mixed $model Related Model or Object * @param array $arguments Arguments to the where method * @return DataMapper Returns self for method chaining. */ private function _get_by_related($model, $arguments = array()) { if ( ! empty($model)) { // Add model to start of arguments $arguments = array_merge(array($model), $arguments); } $this->_related('where', $arguments); return $this->get(); } // -------------------------------------------------------------------- /** * Handles the adding the related part of a query if $parent is set * * @ignore * @return bool Success or failure */ protected function _handle_related() { if ( ! empty($this->parent)) { $has_many = array_key_exists($this->parent['model'], $this->has_many); $has_one = array_key_exists($this->parent['model'], $this->has_one); // If this is a "has many" or "has one" related item if ($has_many || $has_one) { if( ! $this->_get_relation($this->parent['model'], $this->parent['id'])) { return FALSE; } } else { // provide feedback on errors $parent = $this->parent['model']; $this_model = get_class($this); show_error("DataMapper Error: '$parent' is not a valid parent relationship for $this_model. Are your relationships configured correctly?"); } } return TRUE; } // -------------------------------------------------------------------- /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Active Record methods * * * * The following are methods used to provide Active Record * * functionality for data retrieval. * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ // -------------------------------------------------------------------- /** * Add Table Name * * Adds the table name to a field if necessary * * @param string $field Field to add the table name to. * @return string Possibly modified field name. */ public function add_table_name($field) { // only add table if the field doesn't contain a dot (.) or open parentheses if (preg_match('/[\.\(]/', $field) == 0) { // split string into parts, add field $field_parts = explode(',', $field); $field = ''; foreach ($field_parts as $part) { if ( ! empty($field)) { $field .= ', '; } $part = ltrim($part); // handle comparison operators on where $subparts = explode(' ', $part, 2); if ($subparts[0] == '*' || in_array($subparts[0], $this->fields)) { $field .= $this->table . '.' . $part; } else { $field .= $part; } } } return $field; } // -------------------------------------------------------------------- /** * Creates a SQL-function with the given (optional) arguments. * * Each argument can be one of several forms: * 1) An un escaped string value, which will be automatically escaped: "hello" * 2) An escaped value or non-string, which is copied directly: "'hello'" 123, etc * 3) An operator, *, or a non-escaped string is copied directly: "[non-escaped]" ">", etc * 4) A field on this model: "@property" (Also, "@" will be copied directly * 5) A field on a related or deeply related model: "@model/property" "@model/other_model/property" * 6) An array, which is processed recursively as a forumla. * * @param string $function_name Function name. * @param mixed $args,... (Optional) Any commands that need to be passed to the function. * @return string The new SQL function string. */ public function func($function_name) { $ret = $function_name . '('; $args = func_get_args(); // pop the function name array_shift($args); $comma = ''; foreach($args as $arg) { $ret .= $comma . $this->_process_function_arg($arg); if(empty($comma)) { $comma = ', '; } } $ret .= ')'; return $ret; } // private method to convert function arguments into SQL private function _process_function_arg($arg, $is_formula = FALSE) { $ret = ''; if(is_array($arg)) { // formula foreach($arg as $func => $formula_arg) { if(!empty($ret)) { $ret .= ' '; } if(is_numeric($func)) { // process non-functions $ret .= $this->_process_function_arg($formula_arg, TRUE); } else { // recursively process functions within functions $func_args = array_merge(array($func), (array)$formula_arg); $ret .= call_user_func_array(array($this, 'func'), $func_args); } } return $ret; } $operators = array( 'AND', 'OR', 'NOT', // binary logic '<', '>', '<=', '>=', '=', '<>', '!=', // comparators '+', '-', '*', '/', '%', '^', // basic maths '|/', '||/', '!', '!!', '@', '&', '|', '#', '~', // advanced maths '<<', '>>'); // binary operators if(is_string($arg)) { if( ($is_formula && in_array($arg, $operators)) || $arg == '*' || ($arg[0] == "'" && $arg[strlen($arg)-1] == "'") || ($arg[0] == "[" && $arg[strlen($arg)-1] == "]") ) { // simply add already-escaped strings, the special * value, or operators in formulas if($arg[0] == "[" && $arg[strlen($arg)-1] == "]") { // Arguments surrounded by square brackets are added directly, minus the brackets $arg = substr($arg, 1, -1); } $ret .= $arg; } else if($arg[0] == '@') { // model or sub-model property $arg = substr($arg, 1); if(strpos($arg, '/') !== FALSE) { // related property if(strpos($arg, 'parent/') === 0) { // special parent property for subqueries $ret .= str_replace('parent/', '${parent}.', $arg); } else { $rel_elements = explode('/', $arg); $property = array_pop($rel_elements); $table = $this->_add_related_table(implode('/', $rel_elements)); $ret .= $this->db->protect_identifiers($table . '.' . $property); } } else { $ret .= $this->db->protect_identifiers($this->add_table_name($arg)); } } else { $ret .= $this->db->escape($arg); } } else { $ret .= $arg; } return $ret; } // -------------------------------------------------------------------- /** * Used by the magic method for select_func, {where}_func, etc * * @ignore * @param object $query Name of query function * @param array $args Arguments for func() * @return DataMapper Returns self for method chaining. */ private function _func($query, $args) { if(count($args) < 2) { throw new Exception("Invalid number of arguments to {$query}_func: must be at least 2 arguments."); } if($query == 'select') { $alias = array_pop($args); $value = call_user_func_array(array($this, 'func'), $args); $value .= " AS $alias"; // we can't use the normal select method, because CI likes to breaky $this->_add_to_select_directly($value); return $this; } else { $param = array_pop($args); $value = call_user_func_array(array($this, 'func'), $args); return $this->{$query}($value, $param); } } // -------------------------------------------------------------------- /** * Used by the magic method for {where}_field_func, etc. * * @ignore * @param string $query Name of query function * @param array $args Arguments for func() * @return DataMapper Returns self for method chaining. */ private function _field_func($query, $args) { if(count($args) < 2) { throw new Exception("Invalid number of arguments to {$query}_field_func: must be at least 2 arguments."); } $field = array_shift($args); $func = call_user_func_array(array($this, 'func'), $args); return $this->_process_special_query_clause($query, $field, $func); } // -------------------------------------------------------------------- /** * Used by the magic method for select_subquery {where}_subquery, etc * * @ignore * @param string $query Name of query function * @param array $args Arguments for subquery * @return DataMapper Returns self for method chaining. */ private function _subquery($query, $args) { if(count($args) < 1) { throw new Exception("Invalid arguments on {$query}_subquery: must be at least one argument."); } if($query == 'select') { if(count($args) < 2) { throw new Exception('Invalid number of arguments to select_subquery: must be exactly 2 arguments.'); } $sql = $this->_parse_subquery_object($args[0]); $alias = $args[1]; // we can't use the normal select method, because CI likes to breaky $this->_add_to_select_directly("$sql AS $alias"); return $this; } else { $object = $field = $value = NULL; if(is_object($args[0]) || (is_string($args[0]) && !isset($args[1])) ) { $field = $this->_parse_subquery_object($args[0]); if(isset($args[1])) { $value = $this->db->protect_identifiers($this->add_table_name($args[1])); } } else { $field = $this->add_table_name($args[0]); $value = $args[1]; if(is_object($value)) { $value = $this->_parse_subquery_object($value); } } $extra = NULL; if(isset($args[2])) { $extra = $args[2]; } return $this->_process_special_query_clause($query, $field, $value, $extra); } } // -------------------------------------------------------------------- /** * Parses and protects a subquery. * Automatically replaces the special ${parent} argument with a reference to * this table. * * Also replaces all table references that would overlap with this object. * * @ignore * @param object $sql SQL string to process * @return string Processed SQL string. */ protected function _parse_subquery_object($sql) { if(is_object($sql)) { $sql = '(' . $sql->get_sql() . ')'; } // Table Name pattern should be $tablename = $this->db->_escape_identifiers($this->table); $table_pattern = '(?:' . preg_quote($this->table) . '|' . preg_quote($tablename) . ')'; $fieldname = $this->db->_escape_identifiers('__field__'); $field_pattern = '([-\w]+|' . str_replace('__field__', '[-\w]+', preg_quote($fieldname)) . ')'; // replace all table.field references // pattern ends up being [^_](table|`table`).(field|`field`) // the NOT _ at the beginning is to prevent replacing of advanced relationship table references. $pattern = '/([^_])' . $table_pattern . '\.' . $field_pattern . '/i'; // replacement ends up being `table_subquery`.`$1` $replacement = '$1' . $this->db->_escape_identifiers($this->table . '_subquery') . '.$2'; $sql = preg_replace($pattern, $replacement, $sql); // now replace all "table table" aliases // important: the space at the end is required $pattern = "/$table_pattern $table_pattern /i"; $replacement = $tablename . ' ' . $this->db->_escape_identifiers($this->table . '_subquery') . ' '; $sql = preg_replace($pattern, $replacement, $sql); // now replace "FROM table" for self relationships $pattern = "/FROM $table_pattern([,\\s])/i"; $replacement = "FROM $tablename " . $this->db->_escape_identifiers($this->table . '_subquery') . '$1'; $sql = preg_replace($pattern, $replacement, $sql); $sql = str_replace("\n", "\n\t", $sql); return str_replace('${parent}', $this->table, $sql); } // -------------------------------------------------------------------- /** * Manually adds an item to the SELECT column, to prevent it from * being broken by AR->select * * @ignore * @param string $value New SELECT value */ protected function _add_to_select_directly($value) { // copied from system/database/DB_activerecord.php $this->db->ar_select[] = $value; if ($this->db->ar_caching === TRUE) { $this->ar_cache_select[] = $value; $this->ar_cache_exists[] = 'select'; } } // -------------------------------------------------------------------- /** * Handles specialized where clauses, like subqueries and functions * * @ignore * @param string $query Query function * @param string $field Field for Query function * @param mixed $value Value for Query function * @param mixed $extra If included, overrides the default assumption of FALSE for the third parameter to $query * @return DataMapper Returns self for method chaining. */ private function _process_special_query_clause($query, $field, $value, $extra = NULL) { if(strpos($query, 'where_in') !== FALSE) { $query = str_replace('_in', '', $query); $field .= ' IN '; } else if(strpos($query, 'where_not_in') !== FALSE) { $query = str_replace('_not_in', '', $query); $field .= ' NOT IN '; } if(is_null($extra)) { $extra = FALSE; } return $this->{$query}($field, $value, $extra); } // -------------------------------------------------------------------- /** * Select * * Sets the SELECT portion of the query. * * @param mixed $select Field(s) to select, array or comma separated string * @param bool $escape If FALSE, don't escape this field (Probably won't work) * @return DataMapper Returns self for method chaining. */ public function select($select = '*', $escape = NULL) { if ($escape !== FALSE) { if (!is_array($select)) { $select = $this->add_table_name($select); } else { $updated = array(); foreach ($select as $sel) { $updated = $this->add_table_name($sel); } $select = $updated; } } $this->db->select($select, $escape); // For method chaining return $this; } // -------------------------------------------------------------------- /** * Select Max * * Sets the SELECT MAX(field) portion of a query. * * @param string $select Field to look at. * @param string $alias Alias of the MAX value. * @return DataMapper Returns self for method chaining. */ public function select_max($select = '', $alias = '') { // Check if this is a related object if ( ! empty($this->parent)) { $alias = ($alias != '') ? $alias : $select; } $this->db->select_max($this->add_table_name($select), $alias); // For method chaining return $this; } // -------------------------------------------------------------------- /** * Select Min * * Sets the SELECT MIN(field) portion of a query. * * @param string $select Field to look at. * @param string $alias Alias of the MIN value. * @return DataMapper Returns self for method chaining. */ public function select_min($select = '', $alias = '') { // Check if this is a related object if ( ! empty($this->parent)) { $alias = ($alias != '') ? $alias : $select; } $this->db->select_min($this->add_table_name($select), $alias); // For method chaining return $this; } // -------------------------------------------------------------------- /** * Select Avg * * Sets the SELECT AVG(field) portion of a query. * * @param string $select Field to look at. * @param string $alias Alias of the AVG value. * @return DataMapper Returns self for method chaining. */ public function select_avg($select = '', $alias = '') { // Check if this is a related object if ( ! empty($this->parent)) { $alias = ($alias != '') ? $alias : $select; } $this->db->select_avg($this->add_table_name($select), $alias); // For method chaining return $this; } // -------------------------------------------------------------------- /** * Select Sum * * Sets the SELECT SUM(field) portion of a query. * * @param string $select Field to look at. * @param string $alias Alias of the SUM value. * @return DataMapper Returns self for method chaining. */ public function select_sum($select = '', $alias = '') { // Check if this is a related object if ( ! empty($this->parent)) { $alias = ($alias != '') ? $alias : $select; } $this->db->select_sum($this->add_table_name($select), $alias); // For method chaining return $this; } // -------------------------------------------------------------------- /** * Distinct * * Sets the flag to add DISTINCT to the query. * * @param bool $value Set to FALSE to turn back off DISTINCT * @return DataMapper Returns self for method chaining. */ public function distinct($value = TRUE) { $this->db->distinct($value); // For method chaining return $this; } // -------------------------------------------------------------------- /** * Get Where * * Get items matching the where clause. * * @param mixed $where See where() * @param integer|NULL $limit Limit the number of results. * @param integer|NULL $offset Offset the results when limiting. * @return DataMapper Returns self for method chaining. */ public function get_where($where = array(), $limit = NULL, $offset = NULL) { $this->where($where); return $this->get($limit, $offset); } // -------------------------------------------------------------------- /** * Starts a query group. * * @param string $not (Internal use only) * @param string $type (Internal use only) * @return DataMapper Returns self for method chaining. */ public function group_start($not = '', $type = 'AND ') { // in case groups are being nested $type = $this->_get_prepend_type($type); $prefix = (count($this->db->ar_where) == 0 AND count($this->db->ar_cache_where) == 0) ? '' : $type; $this->db->ar_where[] = $prefix . $not . ' ('; $this->_where_group_started = TRUE; return $this; } // -------------------------------------------------------------------- /** * Starts a query group, but ORs the group * @return DataMapper Returns self for method chaining. */ public function or_group_start() { return $this->group_start('', 'OR '); } // -------------------------------------------------------------------- /** * Starts a query group, but NOTs the group * @return DataMapper Returns self for method chaining. */ public function not_group_start() { return $this->group_start('NOT ', 'OR '); } // -------------------------------------------------------------------- /** * Starts a query group, but OR NOTs the group * @return DataMapper Returns self for method chaining. */ public function or_not_group_start() { return $this->group_start('NOT ', 'OR '); } // -------------------------------------------------------------------- /** * Ends a query group. * @return DataMapper Returns self for method chaining. */ public function group_end() { $this->db->ar_where[] = ')'; $this->_where_group_started = FALSE; return $this; } // -------------------------------------------------------------------- /** * protected function to convert the AND or OR prefix to '' when starting * a group. * * @ignore * @param object $type Current type value * @return New type value */ protected function _get_prepend_type($type) { if($this->_where_group_started) { $type = ''; $this->_where_group_started = FALSE; } return $type; } // -------------------------------------------------------------------- /** * Where * * Sets the WHERE portion of the query. * Separates multiple calls with AND. * * Called by get_where() * * @param mixed $key A field or array of fields to check. * @param mixed $value For a single field, the value to compare to. * @param bool $escape If FALSE, the field is not escaped. * @return DataMapper Returns self for method chaining. */ public function where($key, $value = NULL, $escape = TRUE) { return $this->_where($key, $value, 'AND ', $escape); } // -------------------------------------------------------------------- /** * Or Where * * Sets the WHERE portion of the query. * Separates multiple calls with OR. * * @param mixed $key A field or array of fields to check. * @param mixed $value For a single field, the value to compare to. * @param bool $escape If FALSE, the field is not escaped. * @return DataMapper Returns self for method chaining. */ public function or_where($key, $value = NULL, $escape = TRUE) { return $this->_where($key, $value, 'OR ', $escape); } // -------------------------------------------------------------------- /** * Where * * Called by where() or or_where(). * * @ignore * @param mixed $key A field or array of fields to check. * @param mixed $value For a single field, the value to compare to. * @param string $type Type of addition (AND or OR) * @param bool $escape If FALSE, the field is not escaped. * @return DataMapper Returns self for method chaining. */ protected function _where($key, $value = NULL, $type = 'AND ', $escape = NULL) { if ( ! is_array($key)) { $key = array($key => $value); } foreach ($key as $k => $v) { $new_k = $this->add_table_name($k); if ($new_k != $k) { $key[$new_k] = $v; unset($key[$k]); } } $type = $this->_get_prepend_type($type); $this->db->_where($key, $value, $type, $escape); // For method chaining return $this; } // -------------------------------------------------------------------- /** * Where In * * Sets the WHERE field IN ('item', 'item') SQL query joined with * AND if appropriate. * * @param string $key A field to check. * @param array $values An array of values to compare against * @return DataMapper Returns self for method chaining. */ public function where_in($key = NULL, $values = NULL) { return $this->_where_in($key, $values); } // -------------------------------------------------------------------- /** * Or Where In * * Sets the WHERE field IN ('item', 'item') SQL query joined with * OR if appropriate. * * @param string $key A field to check. * @param array $values An array of values to compare against * @return DataMapper Returns self for method chaining. */ public function or_where_in($key = NULL, $values = NULL) { return $this->_where_in($key, $values, FALSE, 'OR '); } // -------------------------------------------------------------------- /** * Where Not In * * Sets the WHERE field NOT IN ('item', 'item') SQL query joined with * AND if appropriate. * * @param string $key A field to check. * @param array $values An array of values to compare against * @return DataMapper Returns self for method chaining. */ public function where_not_in($key = NULL, $values = NULL) { return $this->_where_in($key, $values, TRUE); } // -------------------------------------------------------------------- /** * Or Where Not In * * Sets the WHERE field NOT IN ('item', 'item') SQL query joined wuth * OR if appropriate. * * @param string $key A field to check. * @param array $values An array of values to compare against * @return DataMapper Returns self for method chaining. */ public function or_where_not_in($key = NULL, $values = NULL) { return $this->_where_in($key, $values, TRUE, 'OR '); } // -------------------------------------------------------------------- /** * Where In * * Called by where_in(), or_where_in(), where_not_in(), or or_where_not_in(). * * @ignore * @param string $key A field to check. * @param array $values An array of values to compare against * @param bool $not If TRUE, use NOT IN instead of IN. * @param string $type The type of connection (AND or OR) * @return DataMapper Returns self for method chaining. */ protected function _where_in($key = NULL, $values = NULL, $not = FALSE, $type = 'AND ') { $type = $this->_get_prepend_type($type); if ($values instanceOf DataMapper) { $arr = array(); foreach ($values as $value) { $arr[] = $value->id; } $values = $arr; } $this->db->_where_in($this->add_table_name($key), $values, $not, $type); // For method chaining return $this; } // -------------------------------------------------------------------- /** * Like * * Sets the %LIKE% portion of the query. * Separates multiple calls with AND. * * @param mixed $field A field or array of fields to check. * @param mixed $match For a single field, the value to compare to. * @param string $side One of 'both', 'before', or 'after' * @return DataMapper Returns self for method chaining. */ public function like($field, $match = '', $side = 'both') { return $this->_like($field, $match, 'AND ', $side); } // -------------------------------------------------------------------- /** * Not Like * * Sets the NOT LIKE portion of the query. * Separates multiple calls with AND. * * @param mixed $field A field or array of fields to check. * @param mixed $match For a single field, the value to compare to. * @param string $side One of 'both', 'before', or 'after' * @return DataMapper Returns self for method chaining. */ public function not_like($field, $match = '', $side = 'both') { return $this->_like($field, $match, 'AND ', $side, 'NOT'); } // -------------------------------------------------------------------- /** * Or Like * * Sets the %LIKE% portion of the query. * Separates multiple calls with OR. * * @param mixed $field A field or array of fields to check. * @param mixed $match For a single field, the value to compare to. * @param string $side One of 'both', 'before', or 'after' * @return DataMapper Returns self for method chaining. */ public function or_like($field, $match = '', $side = 'both') { return $this->_like($field, $match, 'OR ', $side); } // -------------------------------------------------------------------- /** * Or Not Like * * Sets the NOT LIKE portion of the query. * Separates multiple calls with OR. * * @param mixed $field A field or array of fields to check. * @param mixed $match For a single field, the value to compare to. * @param string $side One of 'both', 'before', or 'after' * @return DataMapper Returns self for method chaining. */ public function or_not_like($field, $match = '', $side = 'both') { return $this->_like($field, $match, 'OR ', $side, 'NOT'); } // -------------------------------------------------------------------- /** * ILike * * Sets the case-insensitive %LIKE% portion of the query. * * @param mixed $field A field or array of fields to check. * @param mixed $match For a single field, the value to compare to. * @param string $side One of 'both', 'before', or 'after' * @return DataMapper Returns self for method chaining. */ public function ilike($field, $match = '', $side = 'both') { return $this->_like($field, $match, 'AND ', $side, '', TRUE); } // -------------------------------------------------------------------- /** * Not ILike * * Sets the case-insensitive NOT LIKE portion of the query. * Separates multiple calls with AND. * * @param mixed $field A field or array of fields to check. * @param mixed $match For a single field, the value to compare to. * @param string $side One of 'both', 'before', or 'after' * @return DataMapper Returns self for method chaining. */ public function not_ilike($field, $match = '', $side = 'both') { return $this->_like($field, $match, 'AND ', $side, 'NOT', TRUE); } // -------------------------------------------------------------------- /** * Or Like * * Sets the case-insensitive %LIKE% portion of the query. * Separates multiple calls with OR. * * @param mixed $field A field or array of fields to check. * @param mixed $match For a single field, the value to compare to. * @param string $side One of 'both', 'before', or 'after' * @return DataMapper Returns self for method chaining. */ public function or_ilike($field, $match = '', $side = 'both') { return $this->_like($field, $match, 'OR ', $side, '', TRUE); } // -------------------------------------------------------------------- /** * Or Not Like * * Sets the case-insensitive NOT LIKE portion of the query. * Separates multiple calls with OR. * * @param mixed $field A field or array of fields to check. * @param mixed $match For a single field, the value to compare to. * @param string $side One of 'both', 'before', or 'after' * @return DataMapper Returns self for method chaining. */ public function or_not_ilike($field, $match = '', $side = 'both') { return $this->_like($field, $match, 'OR ', $side, 'NOT', TRUE); } // -------------------------------------------------------------------- /** * _Like * * Private function to do actual work. * NOTE: this does NOT use the built-in ActiveRecord LIKE function. * * @ignore * @param mixed $field A field or array of fields to check. * @param mixed $match For a single field, the value to compare to. * @param string $type The type of connection (AND or OR) * @param string $side One of 'both', 'before', or 'after' * @param string $not 'NOT' or '' * @param bool $no_case If TRUE, configure to ignore case. * @return DataMapper Returns self for method chaining. */ protected function _like($field, $match = '', $type = 'AND ', $side = 'both', $not = '', $no_case = FALSE) { if ( ! is_array($field)) { $field = array($field => $match); } foreach ($field as $k => $v) { $new_k = $this->add_table_name($k); if ($new_k != $k) { $field[$new_k] = $v; unset($field[$k]); } } // Taken from CodeIgniter's Active Record because (for some reason) // it is stored separately that normal where statements. foreach ($field as $k => $v) { if($no_case) { $k = 'UPPER(' . $this->db->protect_identifiers($k) .')'; $v = strtoupper($v); } $f = "$k $not LIKE"; if ($side == 'before') { $m = "%{$v}"; } elseif ($side == 'after') { $m = "{$v}%"; } else { $m = "%{$v}%"; } $this->_where($f, $m, $type); } // For method chaining return $this; } // -------------------------------------------------------------------- /** * Group By * * Sets the GROUP BY portion of the query. * * @param string $by Field to group by * @return DataMapper Returns self for method chaining. */ public function group_by($by) { $this->db->group_by($this->add_table_name($by)); // For method chaining return $this; } // -------------------------------------------------------------------- /** * Having * * Sets the HAVING portion of the query. * Separates multiple calls with AND. * * @param string $key Field to compare. * @param string $value value to compare to. * @param bool $escape If FALSE, don't escape the value. * @return DataMapper Returns self for method chaining. */ public function having($key, $value = '', $escape = TRUE) { return $this->_having($key, $value, 'AND ', $escape); } // -------------------------------------------------------------------- /** * Or Having * * Sets the OR HAVING portion of the query. * Separates multiple calls with OR. * * @param string $key Field to compare. * @param string $value value to compare to. * @param bool $escape If FALSE, don't escape the value. * @return DataMapper Returns self for method chaining. */ public function or_having($key, $value = '', $escape = TRUE) { return $this->_having($key, $value, 'OR ', $escape); } // -------------------------------------------------------------------- /** * Having * * Sets the HAVING portion of the query. * Separates multiple calls with AND. * * @ignore * @param string $key Field to compare. * @param string $value value to compare to. * @param string $type Type of connection (AND or OR) * @param bool $escape If FALSE, don't escape the value. * @return DataMapper Returns self for method chaining. */ protected function _having($key, $value = '', $type = 'AND ', $escape = TRUE) { $this->db->_having($this->add_table_name($key), $value, $type, $escape); // For method chaining return $this; } // -------------------------------------------------------------------- /** * Order By * * Sets the ORDER BY portion of the query. * * @param string $orderby Field to order by * @param string $direction One of 'ASC' or 'DESC' Defaults to 'ASC' * @return DataMapper Returns self for method chaining. */ public function order_by($orderby, $direction = '') { $this->db->order_by($this->add_table_name($orderby), $direction); // For method chaining return $this; } // -------------------------------------------------------------------- /** * Adds in the defaut order_by items, if there are any, and * order_by hasn't been overridden. * @ignore */ protected function _handle_default_order_by() { if(empty($this->default_order_by)) { return; } $sel = $this->table . '.' . '*'; $sel_protect = $this->db->protect_identifiers($sel); // only add the items if there isn't an existing order_by, // AND the select statement is empty or includes * or table.* or `table`.* if(empty($this->db->ar_orderby) && ( empty($this->db->ar_select) || in_array('*', $this->db->ar_select) || in_array($sel_protect, $this->db->ar_select) || in_array($sel, $this->db->ar_select) )) { foreach($this->default_order_by as $k => $v) { if(is_int($k)) { $k = $v; $v = ''; } $k = $this->add_table_name($k); $this->order_by($k, $v); } } } // -------------------------------------------------------------------- /** * Limit * * Sets the LIMIT portion of the query. * * @param integer $limit Limit the number of results. * @param integer|NULL $offset Offset the results when limiting. * @return DataMapper Returns self for method chaining. */ public function limit($value, $offset = '') { $this->db->limit($value, $offset); // For method chaining return $this; } // -------------------------------------------------------------------- /** * Offset * * Sets the OFFSET portion of the query. * * @param integer $offset Offset the results when limiting. * @return DataMapper Returns self for method chaining. */ public function offset($offset) { $this->db->offset($offset); // For method chaining return $this; } // -------------------------------------------------------------------- /** * Start Cache * * Starts AR caching. */ public function start_cache() { $this->db->start_cache(); } // -------------------------------------------------------------------- /** * Stop Cache * * Stops AR caching. */ public function stop_cache() { $this->db->stop_cache(); } // -------------------------------------------------------------------- /** * Flush Cache * * Empties the AR cache. */ public function flush_cache() { $this->db->flush_cache(); } // -------------------------------------------------------------------- /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Transaction methods * * * * The following are methods used for transaction handling. * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ // -------------------------------------------------------------------- /** * Trans Off * * This permits transactions to be disabled at run-time. * */ public function trans_off() { $this->db->trans_enabled = FALSE; } // -------------------------------------------------------------------- /** * Trans Strict * * When strict mode is enabled, if you are running multiple groups of * transactions, if one group fails all groups will be rolled back. * If strict mode is disabled, each group is treated autonomously, meaning * a failure of one group will not affect any others. * * @param bool $mode Set to false to disable strict mode. */ public function trans_strict($mode = TRUE) { $this->db->trans_strict($mode); } // -------------------------------------------------------------------- /** * Trans Start * * Start a transaction. * * @param bool $test_mode Set to TRUE to only run a test (and not commit) */ public function trans_start($test_mode = FALSE) { $this->db->trans_start($test_mode); } // -------------------------------------------------------------------- /** * Trans Complete * * Complete a transaction. * * @return bool Success or Failure */ public function trans_complete() { return $this->db->trans_complete(); } // -------------------------------------------------------------------- /** * Trans Begin * * Begin a transaction. * * @param bool $test_mode Set to TRUE to only run a test (and not commit) * @return bool Success or Failure */ public function trans_begin($test_mode = FALSE) { return $this->db->trans_begin($test_mode); } // -------------------------------------------------------------------- /** * Trans Status * * Lets you retrieve the transaction flag to determine if it has failed. * * @return bool Returns FALSE if the transaction has failed. */ public function trans_status() { return $this->_trans_status; } // -------------------------------------------------------------------- /** * Trans Commit * * Commit a transaction. * * @return bool Success or Failure */ public function trans_commit() { return $this->db->trans_commit(); } // -------------------------------------------------------------------- /** * Trans Rollback * * Rollback a transaction. * * @return bool Success or Failure */ public function trans_rollback() { return $this->db->trans_rollback(); } // -------------------------------------------------------------------- /** * Auto Trans Begin * * Begin an auto transaction if enabled. * */ protected function _auto_trans_begin() { // Begin auto transaction if ($this->auto_transaction) { $this->trans_begin(); } } // -------------------------------------------------------------------- /** * Auto Trans Complete * * Complete an auto transaction if enabled. * * @param string $label Name for this transaction. */ protected function _auto_trans_complete($label = 'complete') { // Complete auto transaction if ($this->auto_transaction) { // Check if successful if (!$this->trans_complete()) { $rule = 'transaction'; // Get corresponding error from language file if (FALSE === ($line = $this->lang->line($rule))) { $line = 'Unable to access the ' . $rule .' error message.'; } // Add transaction error message $this->error_message($rule, sprintf($line, $label)); // Set validation as failed $this->valid = FALSE; } } } // -------------------------------------------------------------------- /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Related methods * * * * The following are methods used for managing related records. * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ // -------------------------------------------------------------------- /** * get_related_properties * * Located the relationship properties for a given field or model * Can also optionally attempt to convert the $related_field to * singular, and look up on that. It will modify the $related_field if * the conversion to singular returns a result. * * $related_field can also be a deep relationship, such as * 'post/editor/group', in which case the $related_field will be processed * recursively, and the return value will be $user->has_NN['group']; * * @ignore * @param mixed $related_field Name of related field or related object. * @param bool $try_singular If TRUE, automatically tries to look for a singular name if not found. * @return array Associative array of related properties. */ public function _get_related_properties(&$related_field, $try_singular = FALSE) { // Handle deep relationships if(strpos($related_field, '/') !== FALSE) { $rfs = explode('/', $related_field); $last = $this; $prop = NULL; foreach($rfs as &$rf) { $prop = $last->_get_related_properties($rf, $try_singular); if(is_null($prop)) { break; } $last =& $last->_get_without_auto_populating($rf); } if( ! is_null($prop)) { // update in case any items were converted to singular. $related_field = implode('/', $rfs); } return $prop; } else { if (isset($this->has_many[$related_field])) { return $this->has_many[$related_field]; } else if (isset($this->has_one[$related_field])) { return $this->has_one[$related_field]; } else { if($try_singular) { $rf = singular($related_field); $ret = $this->_get_related_properties($rf); if( is_null($ret)) { show_error("Unable to relate {$this->model} with $related_field."); } else { $related_field = $rf; return $ret; } } else { // not related return NULL; } } } } // -------------------------------------------------------------------- /** * Add Related Table * * Adds the table of a related item, and joins it to this class. * Returns the name of that table for further queries. * * If $related_field is deep, then this adds all necessary relationships * to the query. * * @ignore * @param mixed $object The object (or related field) to look up. * @param string $related_field Related field name for object * @param string $id_only Private, do not use. * @param object $db Private, do not use. * @param array $query_related Private, do not use. * @param string $name_prepend Private, do not use. * @param string $this_table Private, do not use. * @return string Name of the related table, or table.field if ID_Only */ public function _add_related_table($object, $related_field = '', $id_only = FALSE, $db = NULL, &$query_related = NULL, $name_prepend = '', $this_table = NULL) { if ( is_string($object)) { // only a model was passed in, not an object $related_field = $object; $object = NULL; } else if (empty($related_field)) { // model was not passed, so get the Object's native model $related_field = $object->model; } $related_field = strtolower($related_field); // Handle deep relationships if(strpos($related_field, '/') !== FALSE) { $rfs = explode('/', $related_field); $last = $this; $prepend = ''; $object_as = NULL; foreach($rfs as $index => $rf) { // if this is the last item added, we can use the $id_only // shortcut to prevent unnecessarily adding the last table. $temp_id_only = $id_only; if($temp_id_only) { if($index < count($rfs)-1) { $temp_id_only = FALSE; } } $object_as = $last->_add_related_table($rf, '', $temp_id_only, $this->db, $this->_query_related, $prepend, $object_as); $prepend .= $rf . '_'; $last =& $last->_get_without_auto_populating($rf); } return $object_as; } $related_properties = $this->_get_related_properties($related_field); $class = $related_properties['class']; $this_model = $related_properties['join_self_as']; $other_model = $related_properties['join_other_as']; if (empty($object)) { // no object was passed in, so create one $object = new $class(); } if(is_null($query_related)) { $query_related =& $this->_query_related; } if(is_null($this_table)) { $this_table = $this->table; } // Determine relationship table name $relationship_table = $this->_get_relationship_table($object, $related_field); // only add $related_field to the table name if the 'class' and 'related_field' aren't equal // and the related object is in a different table if ( ($class == $related_field) && ($this->table != $object->table) ) { $object_as = $name_prepend . $object->table; $relationship_as = $name_prepend . $relationship_table; } else { $object_as = $name_prepend . $related_field . '_' . $object->table; $relationship_as = $name_prepend . $related_field . '_' . $relationship_table; } $other_column = $other_model . '_id'; $this_column = $this_model . '_id' ; if(is_null($db)) { $db = $this->db; } // Force the selection of the current object's columns if (empty($db->ar_select)) { $db->select($this->table . '.*'); } // the extra in_array column check is for has_one self references if ($relationship_table == $this->table && in_array($other_column, $this->fields)) { // has_one relationship without a join table if($id_only) { // nothing to join, just return the correct data $object_as = $this_table . '.' . $other_column; } else if ( ! in_array($object_as, $query_related)) { $db->join($object->table . ' ' .$object_as, $object_as . '.id = ' . $this_table . '.' . $other_column, 'LEFT OUTER'); $query_related[] = $object_as; } } // the extra in_array column check is for has_one self references else if ($relationship_table == $object->table && in_array($this_column, $object->fields)) { // has_one relationship without a join table if ( ! in_array($object_as, $query_related)) { $db->join($object->table . ' ' .$object_as, $this_table . '.id = ' . $object_as . '.' . $this_column, 'LEFT OUTER'); $query_related[] = $object_as; } if($id_only) { // include the column name $object_as .= '.id'; } } else { // has_one or has_many with a normal join table // Add join if not already included if ( ! in_array($relationship_as, $query_related)) { $db->join($relationship_table . ' ' . $relationship_as, $this_table . '.id = ' . $relationship_as . '.' . $this_column, 'LEFT OUTER'); if($this->_include_join_fields) { $fields = $db->field_data($relationship_table); foreach($fields as $key => $f) { if($f->name == $this_column || $f->name == $other_column) { unset($fields[$key]); } } // add all other fields $selection = ''; foreach ($fields as $field) { $new_field = 'join_'.$field->name; if (!empty($selection)) { $selection .= ', '; } $selection .= $relationship_as.'.'.$field->name.' AS '.$new_field; } $db->select($selection); // now reset the flag $this->_include_join_fields = FALSE; } $query_related[] = $relationship_as; } if($id_only) { // no need to add the whole table $object_as = $relationship_as . '.' . $other_column; } else if ( ! in_array($object_as, $query_related)) { // Add join if not already included $db->join($object->table . ' ' . $object_as, $object_as . '.id = ' . $relationship_as . '.' . $other_column, 'LEFT OUTER'); $query_related[] = $object_as; } } return $object_as; } // -------------------------------------------------------------------- /** * Related * * Sets the specified related query. * * @ignore * @param string $query Query String * @param array $arguments Arguments to process * @param mixed $extra Used to prevent escaping in special circumstances. * @return DataMapper Returns self for method chaining. */ private function _related($query, $arguments = array(), $extra = NULL) { if ( ! empty($query) && ! empty($arguments)) { $object = $field = $value = NULL; $next_arg = 1; // Prepare model if (is_object($arguments[0])) { $object = $arguments[0]; $related_field = $object->model; // Prepare field and value $field = (isset($arguments[1])) ? $arguments[1] : 'id'; $value = (isset($arguments[2])) ? $arguments[2] : $object->id; $next_arg = 3; } else { $related_field = $arguments[0]; // the TRUE allows conversion to singular $related_properties = $this->_get_related_properties($related_field, TRUE); $class = $related_properties['class']; // enables where_related_{model}($object) if(isset($arguments[1]) && is_object($arguments[1])) { $object = $arguments[1]; // Prepare field and value $field = (isset($arguments[2])) ? $arguments[2] : 'id'; $value = (isset($arguments[3])) ? $arguments[3] : $object->id; $next_arg = 4; } else { $object = new $class(); // Prepare field and value $field = (isset($arguments[1])) ? $arguments[1] : 'id'; $value = (isset($arguments[2])) ? $arguments[2] : NULL; $next_arg = 3; } } if($field == 'id') { // special case to prevent joining unecessary tables $field = $this->_add_related_table($object, $related_field, TRUE); } else { // Determine relationship table name, and join the tables $object_table = $this->_add_related_table($object, $related_field); $field = $object_table . '.' . $field; } if(is_string($value) && strpos($value, '${parent}') !== FALSE) { $extra = FALSE; } // allow special arguments to be passed into query methods if(is_null($extra)) { if(isset($arguments[$next_arg])) { $extra = $arguments[$next_arg]; } } // Add query clause if(is_null($extra)) { // convert where to where_in if the value is an array or a DM object if ($query == 'where') { if ( is_array($value) ) { switch(count($value)) { case 0: $value = NULL; break; case 1: $value = reset($value); break; default: $query = 'where_in'; break; } } elseif ( $value instanceOf DataMapper ) { switch($value->result_count()) { case 0: $value = NULL; break; case 1: $value = $value->id; break; default: $query = 'where_in'; break; } } } $this->{$query}($field, $value); } else { $this->{$query}($field, $value, $extra); } } // For method chaining return $this; } // -------------------------------------------------------------------- /** * Magic method to process a subquery for a related object. * The format for this should be * $object->{where}_related_subquery($related_item, $related_field, $subquery) * related_field is optional * * @ignore * @param string $query Query Method * @param object $args Arguments for the query * @return DataMapper Returns self for method chaining. */ private function _related_subquery($query, $args) { $rel_object = $args[0]; $field = $value = NULL; if(isset($args[2])) { $field = $args[1]; $value = $args[2]; } else { $field = 'id'; $value = $args[1]; } if(is_object($value)) { // see 25_activerecord.php $value = $this->_parse_subquery_object($value); } if(strpos($query, 'where_in') !== FALSE) { $query = str_replace('_in', '', $query); $field .= ' IN '; } return $this->_related($query, array($rel_object, $field, $value), FALSE); } // -------------------------------------------------------------------- /** * Is Related To * If this object is related to the provided object, returns TRUE. * Otherwise returns FALSE. * Optionally can be provided a related field and ID. * * @param mixed $related_field The related object or field name * @param int $id ID to compare to if $related_field is a string * @return bool TRUE or FALSE if this object is related to $related_field */ public function is_related_to($related_field, $id = NULL) { if(is_object($related_field)) { $id = $related_field->id; $related_field = $related_field->model; } return ($this->{$related_field}->count(NULL, NULL, $id) > 0); } // -------------------------------------------------------------------- /** * Include Related * * Joins specified values of a has_one object into the current query * If $fields is NULL or '*', then all columns are joined (may require instantiation of the other object) * If $fields is a single string, then just that column is joined. * Otherwise, $fields should be an array of column names. * * $append_name can be used to override the default name to append, or set it to FALSE to prevent appending. * * @param mixed $related_field The related object or field name * @param array $fields The fields to join (NULL or '*' means all fields, or use a single field or array of fields) * @param bool $append_name The name to use for joining (with '_'), or FALSE to disable. * @param bool $instantiate If TRUE, the results are instantiated into objects * @return DataMapper Returns self for method chaining. */ public function include_related($related_field, $fields = NULL, $append_name = TRUE, $instantiate = FALSE) { if (is_object($related_field)) { $object = $related_field; $related_field = $object->model; $related_properties = $this->_get_related_properties($related_field); } else { // the TRUE allows conversion to singular $related_properties = $this->_get_related_properties($related_field, TRUE); $class = $related_properties['class']; $object = new $class(); } if(is_null($fields) || $fields == '*') { $fields = $object->fields; } else if ( ! is_array($fields)) { $fields = array((string)$fields); } $rfs = explode('/', $related_field); $last = $this; foreach($rfs as $rf) { if ( ! isset($last->has_one[$rf]) ) { show_error("Invalid request to include_related: $rf is not a has_one relationship to {$last->model}."); } // prevent populating the related items. $last =& $last->_get_without_auto_populating($rf); } $table = $this->_add_related_table($object, $related_field); $append = ''; if($append_name !== FALSE) { if($append_name === TRUE) { $append = str_replace('/', '_', $related_field); } else { $append = $append_name; } $append .= '_'; } // now add fields $selection = ''; $property_map = array(); foreach ($fields as $field) { $new_field = $append . $field; // prevent collisions if(in_array($new_field, $this->fields)) { if($instantiate && $field == 'id' && $new_field != 'id') { $property_map[$new_field] = $field; } continue; } if (!empty($selection)) { $selection .= ', '; } $selection .= $table.'.'.$field.' AS '.$new_field; if($instantiate) { $property_map[$new_field] = $field; } } if(empty($selection)) { log_message('debug', "DataMapper Warning (include_related): No fields were selected for {$this->model} on $related_field."); } else { if($instantiate) { if(is_null($this->_instantiations)) { $this->_instantiations = array(); } $this->_instantiations[$related_field] = $property_map; } $this->db->select($selection); } // For method chaining return $this; } /** * Legacy version of include_related * DEPRECATED: Will be removed by 2.0 * @deprecated Please use include_related */ public function join_related($related_field, $fields = NULL, $append_name = TRUE) { return $this->include_related($related_field, $fields, $append_name); } // -------------------------------------------------------------------- /** * Includes the number of related items using a subquery. * * Default alias is {$related_field}_count * * @param mixed $related_field Field to count * @param string $alias Alternative alias. * @return DataMapper Returns self for method chaining. */ public function include_related_count($related_field, $alias = NULL) { if (is_object($related_field)) { $object = $related_field; $related_field = $object->model; $related_properties = $this->_get_related_properties($related_field); } else { // the TRUE allows conversion to singular $related_properties = $this->_get_related_properties($related_field, TRUE); $class = $related_properties['class']; $object = new $class(); } if(is_null($alias)) { $alias = $related_field . '_count'; } // Force the selection of the current object's columns if (empty($this->db->ar_select)) { $this->db->select($this->table . '.*'); } // now generate a subquery for counting the related objects $object->select_func('COUNT', '*', 'count'); $this_rel = $related_properties['other_field']; $tablename = $object->_add_related_table($this, $this_rel); $object->where($tablename . '.id = ', $this->db->_escape_identifiers('${parent}.id'), FALSE); $this->select_subquery($object, $alias); return $this; } // -------------------------------------------------------------------- /** * Get Relation * * Finds all related records of this objects current record. * * @ignore * @param mixed $related_field Related field or object * @param int $id ID of related field or object * @return bool Sucess or Failure */ private function _get_relation($related_field, $id) { // No related items if (empty($related_field) || empty($id)) { // Reset query $this->db->_reset_select(); return FALSE; } // To ensure result integrity, group all previous queries if( ! empty($this->db->ar_where)) { array_unshift($this->db->ar_where, '( '); $this->db->ar_where[] = ' )'; } // query all items related to the given model $this->where_related($related_field, 'id', $id); return TRUE; } // -------------------------------------------------------------------- /** * Save Relation * * Saves the relation between this and the other object. * * @ignore * @param DataMapper DataMapper Object to related to this object * @param string Specific related field if necessary. * @return bool Success or Failure */ protected function _save_relation($object, $related_field = '') { if (empty($related_field)) { $related_field = $object->model; } // the TRUE allows conversion to singular $related_properties = $this->_get_related_properties($related_field, TRUE); if ( ! empty($related_properties) && $this->exists() && $object->exists()) { $this_model = $related_properties['join_self_as']; $other_model = $related_properties['join_other_as']; $other_field = $related_properties['other_field']; // Determine relationship table name $relationship_table = $this->_get_relationship_table($object, $related_field); if($relationship_table == $this->table && // catch for self relationships. in_array($other_model . '_id', $this->fields)) { $this->{$other_model . '_id'} = $object->id; $ret = $this->save(); // remove any one-to-one relationships with the other object $this->_remove_other_one_to_one($related_field, $object); return $ret; } else if($relationship_table == $object->table) { $object->{$this_model . '_id'} = $this->id; $ret = $object->save(); // remove any one-to-one relationships with this object $object->_remove_other_one_to_one($other_field, $this); return $ret; } else { $data = array($this_model . '_id' => $this->id, $other_model . '_id' => $object->id); // Check if relation already exists $query = $this->db->get_where($relationship_table, $data, NULL, NULL); if ($query->num_rows() == 0) { // If this object has a "has many" relationship with the other object if (isset($this->has_many[$related_field])) { // If the other object has a "has one" relationship with this object if (isset($object->has_one[$other_field])) { // And it has an existing relation $query = $this->db->get_where($relationship_table, array($other_model . '_id' => $object->id), 1, 0); if ($query->num_rows() > 0) { // Find and update the other objects existing relation to relate with this object $this->db->where($other_model . '_id', $object->id); $this->db->update($relationship_table, $data); } else { // Add the relation since one doesn't exist $this->db->insert($relationship_table, $data); } return TRUE; } else if (isset($object->has_many[$other_field])) { // We can add the relation since this specific relation doesn't exist, and a "has many" to "has many" relationship exists between the objects $this->db->insert($relationship_table, $data); // Self relationships can be defined as reciprocal -- save the reverse relationship at the same time if ($related_properties['reciprocal']) { $data = array($this_model . '_id' => $object->id, $other_model . '_id' => $this->id); $this->db->insert($relationship_table, $data); } return TRUE; } } // If this object has a "has one" relationship with the other object else if (isset($this->has_one[$related_field])) { // And it has an existing relation $query = $this->db->get_where($relationship_table, array($this_model . '_id' => $this->id), 1, 0); if ($query->num_rows() > 0) { // Find and update the other objects existing relation to relate with this object $this->db->where($this_model . '_id', $this->id); $this->db->update($relationship_table, $data); } else { // Add the relation since one doesn't exist $this->db->insert($relationship_table, $data); } return TRUE; } } else { // Relationship already exists return TRUE; } } } else { if( ! $object->exists()) { $msg = 'dm_save_rel_noobj'; } else if( ! $this->exists()) { $msg = 'dm_save_rel_nothis'; } else { $msg = 'dm_save_rel_failed'; } $msg = $this->lang->line($msg); $this->error_message($related_field, sprintf($msg, $related_field)); } return FALSE; } // -------------------------------------------------------------------- /** * Remove Other One-to-One * Removes other relationships on a one-to-one ITFK relationship * * @ignore * @param string $rf Related field to look at. * @param DataMapper $object Object to look at. */ private function _remove_other_one_to_one($rf, $object) { if( ! $object->exists()) { return; } $related_properties = $this->_get_related_properties($rf, TRUE); if( ! array_key_exists($related_properties['other_field'], $object->has_one)) { return; } // This should be a one-to-one relationship with an ITFK if we got this far. $other_column = $related_properties['join_other_as'] . '_id'; $c = get_class($this); $update = new $c(); $update->where($other_column, $object->id); if($this->exists()) { $update->where('id <>', $this->id); } $update->update($other_column, NULL); } // -------------------------------------------------------------------- /** * Delete Relation * * Deletes the relation between this and the other object. * * @ignore * @param DataMapper $object Object to remove the relationship to. * @param string $related_field Optional specific related field * @return bool Success or Failure */ protected function _delete_relation($object, $related_field = '') { if (empty($related_field)) { $related_field = $object->model; } // the TRUE allows conversion to singular $related_properties = $this->_get_related_properties($related_field, TRUE); if ( ! empty($related_properties) && ! empty($this->id) && ! empty($object->id)) { $this_model = $related_properties['join_self_as']; $other_model = $related_properties['join_other_as']; // Determine relationship table name $relationship_table = $this->_get_relationship_table($object, $related_field); if ($relationship_table == $this->table && // catch for self relationships. in_array($other_model . '_id', $this->fields)) { $this->{$other_model . '_id'} = NULL; $this->save(); } else if ($relationship_table == $object->table) { $object->{$this_model . '_id'} = NULL; $object->save(); } else { $data = array($this_model . '_id' => $this->id, $other_model . '_id' => $object->id); // Delete relation $this->db->delete($relationship_table, $data); // Delete reverse direction if a reciprocal self relationship if ($related_properties['reciprocal']) { $data = array($this_model . '_id' => $object->id, $other_model . '_id' => $this->id); $this->db->delete($relationship_table, $data); } } // Clear related object so it is refreshed on next access unset($this->{$related_field}); return TRUE; } return FALSE; } // -------------------------------------------------------------------- /** * Get Relationship Table * * Determines the relationship table between this object and $object. * * @ignore * @param DataMapper $object Object that we are interested in. * @param string $related_field Optional specific related field. * @return string The name of the table this relationship is stored on. */ public function _get_relationship_table($object, $related_field = '') { $prefix = $object->prefix; $table = $object->table; if (empty($related_field)) { $related_field = $object->model; } $related_properties = $this->_get_related_properties($related_field); $this_model = $related_properties['join_self_as']; $other_model = $related_properties['join_other_as']; $other_field = $related_properties['other_field']; if (isset($this->has_one[$related_field])) { // see if the relationship is in this table if (in_array($other_model . '_id', $this->fields)) { return $this->table; } } if (isset($object->has_one[$other_field])) { // see if the relationship is in this table if (in_array($this_model . '_id', $object->fields)) { return $object->table; } } // was a join table defined for this relation? if ( ! empty($related_properties['join_table']) ) { $relationship_table = $related_properties['join_table']; } else { $relationship_table = ''; // Check if self referencing if ($this->table == $table) { // use the model names from related_properties $p_this_model = plural($this_model); $p_other_model = plural($other_model); $relationship_table = ($p_this_model < $p_other_model) ? $p_this_model . '_' . $p_other_model : $p_other_model . '_' . $p_this_model; } else { $relationship_table = ($this->table < $table) ? $this->table . '_' . $table : $table . '_' . $this->table; } // Remove all occurances of the prefix from the relationship table $relationship_table = str_replace($prefix, '', str_replace($this->prefix, '', $relationship_table)); // So we can prefix the beginning, using the join prefix instead, if it is set $relationship_table = (empty($this->join_prefix)) ? $this->prefix . $relationship_table : $this->join_prefix . $relationship_table; } return $relationship_table; } // -------------------------------------------------------------------- /** * Count Related * * Returns the number of related items in the database and in the related object. * Used by the _related_(required|min|max) validation rules. * * @ignore * @param string $related_field The related field. * @param mixed $object Object or array to include in the count. * @return int Number of related items. */ protected function _count_related($related_field, $object = '') { $count = 0; // lookup relationship info // the TRUE allows conversion to singular $rel_properties = $this->_get_related_properties($related_field, TRUE); $class = $rel_properties['class']; $ids = array(); if ( ! empty($object)) { $count = $this->_count_related_objects($related_field, $object, '', $ids); $ids = array_unique($ids); } if ( ! empty($related_field) && ! empty($this->id)) { $one = isset($this->has_one[$related_field]); // don't bother looking up relationships if this is a $has_one and we already have one. if( (!$one) || empty($ids)) { // Prepare model $object = new $class(); // Store parent data $object->parent = array('model' => $rel_properties['other_field'], 'id' => $this->id); // pass in IDs to exclude from the count $count += $object->count($ids); } } return $count; } // -------------------------------------------------------------------- /** * Private recursive function to count the number of objects * in a passed in array (or a single object) * * @ignore * @param string $compare related field (model) to compare to * @param mixed $object Object or array to count * @param string $related_field related field of $object * @param array $ids list of IDs we've already found. * @return int Number of items found. */ private function _count_related_objects($compare, $object, $related_field, &$ids) { $count = 0; if (is_array($object)) { // loop through array to check for objects foreach ($object as $rel_field => $obj) { if ( ! is_string($rel_field)) { // if this object doesn't have a related field, use the parent related field $rel_field = $related_field; } $count += $this->_count_related_objects($compare, $obj, $rel_field, $ids); } } else { // if this object doesn't have a related field, use the model if (empty($related_field)) { $related_field = $object->model; } // if this object is the same relationship type, it counts if ($related_field == $compare && $object->exists()) { $ids[] = $object->id; $count++; } } return $count; } // -------------------------------------------------------------------- /** * Include Join Fields * * If TRUE, the any extra fields on the join table will be included * * @param bool $include If FALSE, turns back off the directive. * @return DataMapper Returns self for method chaining. */ public function include_join_fields($include = TRUE) { $this->_include_join_fields = $include; return $this; } // -------------------------------------------------------------------- /** * Set Join Field * * Sets the value on a join table based on the related field * If $related_field is an array, then the array should be * in the form $related_field => $object or array($object) * * @param mixed $related_field An object or array. * @param mixed $field Field or array of fields to set. * @param mixed $value Value for a single field to set. * @param mixed $object Private for recursion, do not use. * @return DataMapper Returns self for method chaining. */ public function set_join_field($related_field, $field, $value = NULL, $object = NULL) { $related_ids = array(); if (is_array($related_field)) { // recursively call this on the array passed in. foreach ($related_field as $key => $object) { $this->set_join_field($key, $field, $value, $object); } return; } else if (is_object($related_field)) { $object = $related_field; $related_field = $object->model; $related_ids[] = $object->id; $related_properties = $this->_get_related_properties($related_field); } else { // the TRUE allows conversion to singular $related_properties = $this->_get_related_properties($related_field, TRUE); if (is_null($object)) { $class = $related_properties['class']; $object = new $class(); } } // Determine relationship table name $relationship_table = $this->_get_relationship_table($object, $related_field); if (empty($object)) { // no object was passed in, so create one $class = $related_properties['class']; $object = new $class(); } $this_model = $related_properties['join_self_as']; $other_model = $related_properties['join_other_as']; if (! is_array($field)) { $field = array( $field => $value ); } if ( ! is_array($object)) { $object = array($object); } if (empty($object)) { $this->db->where($this_model . '_id', $this->id); $this->db->update($relationship_table, $field); } else { foreach ($object as $obj) { $this->db->where($this_model . '_id', $this->id); $this->db->where($other_model . '_id', $obj->id); $this->db->update($relationship_table, $field); } } // For method chaining return $this; } // -------------------------------------------------------------------- /** * Join Field * * Adds a query of a join table's extra field * Accessed via __call * * @ignore * @param string $query Query method. * @param array $arguments Arguments for query. * @return DataMapper Returns self for method chaining. */ private function _join_field($query, $arguments) { if ( ! empty($query) && count($arguments) >= 3) { $object = $field = $value = NULL; // Prepare model if (is_object($arguments[0])) { $object = $arguments[0]; $related_field = $object->model; } else { $related_field = $arguments[0]; // the TRUE allows conversion to singular $related_properties = $this->_get_related_properties($related_field, TRUE); $class = $related_properties['class']; $object = new $class(); } // Prepare field and value $field = $arguments[1]; $value = $arguments[2]; // Determine relationship table name, and join the tables $rel_table = $this->_get_relationship_table($object, $related_field); // Add query clause $extra = NULL; if(count($arguments) > 3) { $extra = $arguments[3]; } if(is_null($extra)) { $this->{$query}($rel_table . '.' . $field, $value); } else { $this->{$query}($rel_table . '.' . $field, $value, $extra); } } // For method chaining return $this; } // -------------------------------------------------------------------- /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Related Validation methods * * * * The following are methods used to validate the * * relationships of this object. * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ // -------------------------------------------------------------------- /** * Related Required (pre-process) * * Checks if the related object has the required related item * or if the required relation already exists. * * @ignore */ protected function _related_required($object, $model) { return ($this->_count_related($model, $object) == 0) ? FALSE : TRUE; } // -------------------------------------------------------------------- /** * Related Min Size (pre-process) * * Checks if the value of a property is at most the minimum size. * * @ignore */ protected function _related_min_size($object, $model, $size = 0) { return ($this->_count_related($model, $object) < $size) ? FALSE : TRUE; } // -------------------------------------------------------------------- /** * Related Max Size (pre-process) * * Checks if the value of a property is at most the maximum size. * * @ignore */ protected function _related_max_size($object, $model, $size = 0) { return ($this->_count_related($model, $object) > $size) ? FALSE : TRUE; } // -------------------------------------------------------------------- /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Validation methods * * * * The following are methods used to validate the * * values of this objects properties. * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ // -------------------------------------------------------------------- /** * Always Validate * * Does nothing, but forces a validation even if empty (for non-required fields) * * @ignore */ protected function _always_validate() { } // -------------------------------------------------------------------- /** * Alpha Dash Dot (pre-process) * * Alpha-numeric with underscores, dashes and full stops. * * @ignore */ protected function _alpha_dash_dot($field) { return ( ! preg_match('/^([\.-a-z0-9_-])+$/i', $this->{$field})) ? FALSE : TRUE; } // -------------------------------------------------------------------- /** * Alpha Slash Dot (pre-process) * * Alpha-numeric with underscores, dashes, forward slashes and full stops. * * @ignore */ protected function _alpha_slash_dot($field) { return ( ! preg_match('/^([\.\/-a-z0-9_-])+$/i', $this->{$field})) ? FALSE : TRUE; } // -------------------------------------------------------------------- /** * Matches (pre-process) * * Match one field to another. * This replaces the version in CI_Form_validation. * * @ignore */ protected function _matches($field, $other_field) { return ($this->{$field} !== $this->{$other_field}) ? FALSE : TRUE; } // -------------------------------------------------------------------- /** * Min Date (pre-process) * * Checks if the value of a property is at least the minimum date. * * @ignore */ protected function _min_date($field, $date) { return (strtotime($this->{$field}) < strtotime($date)) ? FALSE : TRUE; } // -------------------------------------------------------------------- /** * Max Date (pre-process) * * Checks if the value of a property is at most the maximum date. * * @ignore */ protected function _max_date($field, $date) { return (strtotime($this->{$field}) > strtotime($date)) ? FALSE : TRUE; } // -------------------------------------------------------------------- /** * Min Size (pre-process) * * Checks if the value of a property is at least the minimum size. * * @ignore */ protected function _min_size($field, $size) { return ($this->{$field} < $size) ? FALSE : TRUE; } // -------------------------------------------------------------------- /** * Max Size (pre-process) * * Checks if the value of a property is at most the maximum size. * * @ignore */ protected function _max_size($field, $size) { return ($this->{$field} > $size) ? FALSE : TRUE; } // -------------------------------------------------------------------- /** * Unique (pre-process) * * Checks if the value of a property is unique. * If the property belongs to this object, we can ignore it. * * @ignore */ protected function _unique($field) { if ( ! empty($this->{$field})) { $query = $this->db->get_where($this->table, array($field => $this->{$field}), 1, 0); if ($query->num_rows() > 0) { $row = $query->row(); // If unique value does not belong to this object if ($this->id != $row->id) { // Then it is not unique return FALSE; } } } // No matches found so is unique return TRUE; } // -------------------------------------------------------------------- /** * Unique Pair (pre-process) * * Checks if the value of a property, paired with another, is unique. * If the properties belongs to this object, we can ignore it. * * @ignore */ protected function _unique_pair($field, $other_field = '') { if ( ! empty($this->{$field}) && ! empty($this->{$other_field})) { $query = $this->db->get_where($this->table, array($field => $this->{$field}, $other_field => $this->{$other_field}), 1, 0); if ($query->num_rows() > 0) { $row = $query->row(); // If unique pair value does not belong to this object if ($this->id != $row->id) { // Then it is not a unique pair return FALSE; } } } // No matches found so is unique return TRUE; } // -------------------------------------------------------------------- /** * Valid Date (pre-process) * * Checks whether the field value is a valid DateTime. * * @ignore */ protected function _valid_date($field) { // Ignore if empty if (empty($this->{$field})) { return TRUE; } $date = date_parse($this->{$field}); return checkdate($date['month'], $date['day'],$date['year']); } // -------------------------------------------------------------------- /** * Valid Date Group (pre-process) * * Checks whether the field value, grouped with other field values, is a valid DateTime. * * @ignore */ protected function _valid_date_group($field, $fields = array()) { // Ignore if empty if (empty($this->{$field})) { return TRUE; } $date = date_parse($this->{$fields['year']} . '-' . $this->{$fields['month']} . '-' . $this->{$fields['day']}); return checkdate($date['month'], $date['day'],$date['year']); } // -------------------------------------------------------------------- /** * Valid Match (pre-process) * * Checks whether the field value matches one of the specified array values. * * @ignore */ protected function _valid_match($field, $param = array()) { return in_array($this->{$field}, $param); } // -------------------------------------------------------------------- /** * Boolean (pre-process) * * Forces a field to be either TRUE or FALSE. * Uses PHP's built-in boolean conversion. * * @ignore */ protected function _boolean($field) { $this->{$field} = (boolean)$this->{$field}; } // -------------------------------------------------------------------- /** * Encode PHP Tags (prep) * * Convert PHP tags to entities. * This replaces the version in CI_Form_validation. * * @ignore */ protected function _encode_php_tags($field) { $this->{$field} = encode_php_tags($this->{$field}); } // -------------------------------------------------------------------- /** * Prep for Form (prep) * * Converts special characters to allow HTML to be safely shown in a form. * This replaces the version in CI_Form_validation. * * @ignore */ protected function _prep_for_form($field) { $this->{$field} = $this->form_validation->prep_for_form($this->{$field}); } // -------------------------------------------------------------------- /** * Prep URL (prep) * * Adds "http://" to URLs if missing. * This replaces the version in CI_Form_validation. * * @ignore */ protected function _prep_url($field) { $this->{$field} = $this->form_validation->prep_url($this->{$field}); } // -------------------------------------------------------------------- /** * Strip Image Tags (prep) * * Strips the HTML from image tags leaving the raw URL. * This replaces the version in CI_Form_validation. * * @ignore */ protected function _strip_image_tags($field) { $this->{$field} = strip_image_tags($this->{$field}); } // -------------------------------------------------------------------- /** * XSS Clean (prep) * * Runs the data through the XSS filtering function, described in the Input Class page. * This replaces the version in CI_Form_validation. * * @ignore */ protected function _xss_clean($field, $is_image = FALSE) { $this->{$field} = xss_clean($this->{$field}, $is_image); } // -------------------------------------------------------------------- /** * Trim * Custom trim rule that ignores NULL values * * @ignore */ protected function _trim($field) { if( ! empty($this->{$field})) { $this->{$field} = trim($this->{$field}); } } // -------------------------------------------------------------------- /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Common methods * * * * The following are common methods used by other methods. * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ // -------------------------------------------------------------------- /** * A specialized language lookup function that will automatically * insert the model, table, and (optional) field into a key, and return the * language result for the replaced key. * * @param string $key Basic key to use * @param string $field Optional field value * @return string|bool */ public function localize_by_model($key, $field = NULL) { $s = array('${model}', '${table}'); $r = array($this->model, $this->table); if(!is_null($field)) { $s[] = '${field}'; $r[] = $field; } $key = str_replace($s, $r, $key); return $this->lang->line($key); } // -------------------------------------------------------------------- /** * Variant that handles looking up a field labels * @param string $field Name of field * @param string|bool $label If not FALSE overrides default label. * @return string|bool */ public function localize_label($field, $label = FALSE) { if($label === FALSE) { $label = $field; if(!empty($this->field_label_lang_format)) { $label = $this->localize_by_model($this->field_label_lang_format, $field); if($label === FALSE) { $label = $field; } } } else if(strpos($label, 'lang:') === 0) { $label = $this->localize_by_model(substr($label, 5), $field); } return $label; } // -------------------------------------------------------------------- /** * Allows you to define has_one relations at runtime * @param string name of the model to make a relation with * @param array optional, array with advanced relationship definitions * @return bool */ public function has_one( $parm1 = NULL, $parm2 = NULL ) { if ( is_null($parm1) && is_null($parm2) ) { return FALSE; } elseif ( is_array($parm2) ) { return $this->_relationship('has_one', $parm2, $parm1); } else { return $this->_relationship('has_one', $parm1, 0); } } // -------------------------------------------------------------------- /** * Allows you to define has_many relations at runtime * @param string name of the model to make a relation with * @param array optional, array with advanced relationship definitions * @return bool */ public function has_many( $parm1 = NULL, $parm2 = NULL ) { if ( is_null($parm1) && is_null($parm2) ) { return FALSE; } elseif ( is_array($parm2) ) { return $this->_relationship('has_many', $parm2, $parm1); } else { return $this->_relationship('has_many', $parm1, 0); } } // -------------------------------------------------------------------- /** * Creates or updates the production schema cache file for this model * @param void * @return void */ public function production_cache() { // if requested, store the item to the production cache if( ! empty(DataMapper::$config['production_cache'])) { // check if it's a fully qualified path first if (!is_dir($cache_folder = DataMapper::$config['production_cache'])) { // if not, it's relative to the application path $cache_folder = APPPATH . DataMapper::$config['production_cache']; } if(file_exists($cache_folder) && is_dir($cache_folder) && is_writeable($cache_folder)) { $common_key = DataMapper::$common[DMZ_CLASSNAMES_KEY][strtolower(get_class($this))]; $cache_file = $cache_folder . '/' . $common_key . EXT; $cache = "<"."?php if ( ! defined('BASEPATH')) exit('No direct script access allowed'); \n"; $cache .= '$cache = ' . var_export(DataMapper::$common[$common_key], TRUE) . ';'; if ( ! $fp = @fopen($cache_file, 'w')) { show_error('Error creating production cache file: ' . $cache_file); } flock($fp, LOCK_EX); fwrite($fp, $cache); flock($fp, LOCK_UN); fclose($fp); @chmod($cache_file, FILE_WRITE_MODE); } } } // -------------------------------------------------------------------- /** * Define a new relationship for the current model */ protected function _relationship($type = '', $definition = array(), $name = 0) { // check the parameters if (empty($type) OR ! in_array($type, array('has_one','has_many'))) { return FALSE; } // allow for simple (old-style) associations if (is_int($name)) { // delete the old style entry, we're going to convert it if (isset($this->{$type}[$name])) { unset($this->{$type}[$name]); } $name = $definition; } // get the current relationships $new = (array) $this->{$type}; // convert value into array if necessary if ( ! is_array($definition)) { $definition = array('class' => $definition); } else if ( ! isset($definition['class'])) { // if already an array, ensure that the class attribute is set $definition['class'] = $name; } if( ! isset($definition['other_field'])) { // add this model as the model to use in queries if not set $definition['other_field'] = $this->model; } if( ! isset($definition['join_self_as'])) { // add this model as the model to use in queries if not set $definition['join_self_as'] = $definition['other_field']; } if( ! isset($definition['join_other_as'])) { // add the key as the model to use in queries if not set $definition['join_other_as'] = $name; } if( ! isset($definition['join_table'])) { // by default, automagically determine the join table name $definition['join_table'] = ''; } if(isset($definition['reciprocal'])) { // only allow a reciprocal relationship to be defined if this is a has_many self relationship $definition['reciprocal'] = ($definition['reciprocal'] && $type == 'has_many' && $definition['class'] == strtolower(get_class($this))); } else { $definition['reciprocal'] = FALSE; } $new[$name] = $definition; // load in labels for each not-already-set field if(!isset($this->validation[$name])) { $label = $this->localize_label($name); if(!empty($label)) { // label is re-set below, to prevent caching language-based labels $this->validation[$name] = array('field' => $name, 'rules' => array()); } } // replace the old array $this->{$type} = $new; } // -------------------------------------------------------------------- /** * To Array * * Converts this objects current record into an array for database queries. * If validate is TRUE (getting by objects properties) empty objects are ignored. * * @ignore * @param bool $validate * @return array */ protected function _to_array($validate = FALSE) { $data = array(); foreach ($this->fields as $field) { if ($validate && ! isset($this->{$field})) { continue; } $data[$field] = $this->{$field}; } return $data; } // -------------------------------------------------------------------- /** * Process Query * * Converts a query result into an array of objects. * Also updates this object * * @ignore * @param CI_DB_result $query */ protected function _process_query($query) { if ($query->num_rows() > 0) { // Populate all with records as objects $this->all = array(); $this->_to_object($this, $query->row()); // don't bother recreating the first item. $index = ($this->all_array_uses_ids && isset($this->id)) ? $this->id : 0; $this->all[$index] = $this->get_clone(); if($query->num_rows() > 1) { $model = get_class($this); $first = TRUE; foreach ($query->result() as $row) { if($first) { $first = FALSE; continue; } $item = new $model(); $this->_to_object($item, $row); if($this->all_array_uses_ids && isset($item->id)) { $this->all[$item->id] = $item; } else { $this->all[] = $item; } } } // remove instantiations $this->_instantiations = NULL; // free large queries if($query->num_rows() > $this->free_result_threshold) { $query->free_result(); } } else { // Refresh stored values is called by _to_object normally $this->_refresh_stored_values(); } } // -------------------------------------------------------------------- /** * To Object * Copies the values from a query result row to an object. * Also initializes that object by running get rules, and * refreshing stored values on the object. * * Finally, if any "instantiations" are requested, those related objects * are created off of query results * * This is only public so that the iterator can access it. * * @ignore * @param DataMapper $item Item to configure * @param object $row Query results */ public function _to_object($item, $row) { // Populate this object with values from first record foreach ($row as $key => $value) { $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(); } } } // -------------------------------------------------------------------- /** * Run Get Rules * * Processes values loaded from the database * * @ignore */ protected function _run_get_rules() { // Loop through each property to be validated foreach ($this->_field_tracking['get_rules'] as $field) { // Get validation settings $rules = $this->validation[$field]['get_rules']; // only process non-empty keys that are not specifically // set to be null if( ! isset($this->{$field}) && ! in_array('allow_null', $rules)) { if(isset($this->has_one[$field])) { // automatically process $item_id values $field = $field . '_id'; if( ! isset($this->{$field}) && ! in_array('allow_null', $rules)) { continue; } } else { continue; } } // Loop through each rule to validate this property against foreach ($rules as $rule => $param) { // Check for parameter if (is_numeric($rule)) { $rule = $param; $param = ''; } if($rule == 'allow_null') { continue; } if (method_exists($this, '_' . $rule)) { // Run rule from DataMapper or the class extending DataMapper $result = $this->{'_' . $rule}($field, $param); } else if($this->_extension_method_exists('rule_' . $rule)) { // Run an extension-based rule. $result = $this->{'rule_' . $rule}($field, $param); } else if (method_exists($this->form_validation, $rule)) { // Run rule from CI Form Validation $result = $this->form_validation->{$rule}($this->{$field}, $param); } else if (function_exists($rule)) { // Run rule from PHP $this->{$field} = $rule($this->{$field}); } } } } // -------------------------------------------------------------------- /** * Refresh Stored Values * * Refreshes the stored values with the current values. * * @ignore */ protected function _refresh_stored_values() { // Update stored values foreach ($this->fields as $field) { $this->stored->{$field} = $this->{$field}; } // If there is a "matches" validation rule, match the field value with the other field value foreach ($this->_field_tracking['matches'] as $field_name => $match_name) { $this->{$field_name} = $this->stored->{$field_name} = $this->{$match_name}; } } // -------------------------------------------------------------------- /** * Assign Libraries * * Originally used by CodeIgniter, now just logs a warning. * * @ignore */ public function _assign_libraries() { log_message('debug', "Warning: A DMZ model ({$this->model}) was either loaded via autoload, or manually. DMZ automatically loads models, so this is unnecessary."); } // -------------------------------------------------------------------- /** * Assign Libraries * * Assigns required CodeIgniter libraries to DataMapper. * * @ignore */ protected function _dmz_assign_libraries() { $CI =& get_instance(); if ($CI) { $this->lang = $CI->lang; $this->load = $CI->load; $this->config = $CI->config; } } // -------------------------------------------------------------------- /** * Load Languages * * Loads required language files. * * @ignore */ protected function _load_languages() { // Load the DataMapper language file $this->lang->load('datamapper'); } // -------------------------------------------------------------------- /** * Load Helpers * * Loads required CodeIgniter helpers. * * @ignore */ protected function _load_helpers() { // Load inflector helper for singular and plural functions $this->load->helper('inflector'); // Load security helper for prepping functions $this->load->helper('security'); } } /** * Simple class to prevent errors with unset fields. * @package DMZ * * @param string $FIELD Get the error message for a given field or custom error * @param string $RELATED Get the error message for a given relationship * @param string $transaction Get the transaction error. */ class DM_Error_Object { /** * Array of all error messages. * @var array */ public $all = array(); /** * String containing entire error message. * @var string */ public $string = ''; /** * All unset fields are returned as empty strings by default. * @ignore * @param string $field * @return string Empty string */ public function __get($field) { return ''; } } /** * Iterator for get_iterated * * @package DMZ */ class DM_DatasetIterator implements Iterator, Countable { /** * The parent DataMapper object that contains important info. * @var DataMapper */ protected $parent; /** * The temporary DM object used in the loops. * @var DataMapper */ protected $object; /** * Results array * @var array */ protected $result; /** * Number of results * @var int */ protected $count; /** * Current position * @var int */ protected $pos; /** * @param DataMapper $object Should be cloned ahead of time * @param DB_result $query result from a CI DB query */ function __construct($object, $query) { // store the object as a main object $this->parent = $object; // clone the parent object, so it can be manipulated safely. $this->object = $object->get_clone(); // Now get the information on the current query object $this->result = $query->result(); $this->count = count($this->result); $this->pos = 0; } /** * Gets the item at the current index $pos * @return DataMapper */ function current() { return $this->get($this->pos); } function key() { return $this->pos; } /** * Gets the item at index $index * @param int $index * @return DataMapper */ function get($index) { // clear to ensure that the item is not duplicating data $this->object->clear(); // set the current values on the object $this->parent->_to_object($this->object, $this->result[$index]); return $this->object; } function next() { $this->pos++; } function rewind() { $this->pos = 0; } function valid() { return ($this->pos < $this->count); } /** * Returns the number of results * @return int */ function count() { return $this->count; } // Alias for count(); function result_count() { return $this->count; } } // -------------------------------------------------------------------------- /** * Autoload * * Autoloads object classes that are used with DataMapper. * Must be at end due to implements IteratorAggregate... */ spl_autoload_register('DataMapper::autoload'); /* End of file datamapper.php */ /* Location: ./application/models/datamapper.php */