From 98434eddbbea978a46d03d46282882732e439de6 Mon Sep 17 00:00:00 2001 From: Karsten Heiken Date: Sun, 29 May 2011 14:57:33 +0200 Subject: [PATCH] Include Datamapper ORM --- application/config/autoload.php | 2 +- application/config/datamapper.php | 33 + application/datamapper/array.php | 194 + application/datamapper/csv.php | 225 + application/datamapper/htmlform.php | 793 ++ application/datamapper/json.php | 220 + application/datamapper/nestedsets.php | 1369 ++++ application/datamapper/rowindex.php | 211 + application/datamapper/simplecache.php | 70 + application/datamapper/translate.php | 76 + application/helpers/inflector_helper.php | 236 + application/language/ca/datamapper_lang.php | 25 + .../language/english/datamapper_lang.php | 25 + application/language/es/datamapper_lang.php | 25 + application/language/fr/datamapper_lang.php | 25 + .../language/german/datamapper_lang.php | 25 + application/language/nl/datamapper_lang.php | 25 + .../language/pt_BR/datamapper_lang.php | 25 + application/libraries/datamapper.php | 6488 +++++++++++++++++ 19 files changed, 10091 insertions(+), 1 deletion(-) create mode 100644 application/config/datamapper.php create mode 100644 application/datamapper/array.php create mode 100644 application/datamapper/csv.php create mode 100644 application/datamapper/htmlform.php create mode 100644 application/datamapper/json.php create mode 100644 application/datamapper/nestedsets.php create mode 100644 application/datamapper/rowindex.php create mode 100644 application/datamapper/simplecache.php create mode 100644 application/datamapper/translate.php create mode 100644 application/helpers/inflector_helper.php create mode 100644 application/language/ca/datamapper_lang.php create mode 100644 application/language/english/datamapper_lang.php create mode 100644 application/language/es/datamapper_lang.php create mode 100644 application/language/fr/datamapper_lang.php create mode 100644 application/language/german/datamapper_lang.php create mode 100644 application/language/nl/datamapper_lang.php create mode 100644 application/language/pt_BR/datamapper_lang.php create mode 100644 application/libraries/datamapper.php diff --git a/application/config/autoload.php b/application/config/autoload.php index 29473a9..c57be49 100755 --- a/application/config/autoload.php +++ b/application/config/autoload.php @@ -52,7 +52,7 @@ $autoload['packages'] = array(APPPATH.'third_party'); | $autoload['libraries'] = array('database', 'session', 'xmlrpc'); */ -$autoload['libraries'] = array('lang_detect', 'database', 'session', 'access'); +$autoload['libraries'] = array('lang_detect', 'database', 'session', 'access', 'datamapper'); /* diff --git a/application/config/datamapper.php b/application/config/datamapper.php new file mode 100644 index 0000000..71b8b89 --- /dev/null +++ b/application/config/datamapper.php @@ -0,0 +1,33 @@ +'; +$config['error_suffix'] = '

'; +$config['created_field'] = 'created'; +$config['updated_field'] = 'updated'; +$config['local_time'] = FALSE; +$config['unix_timestamp'] = FALSE; +$config['timestamp_format'] = ''; +$config['lang_file_format'] = 'model_${model}'; +$config['field_label_lang_format'] = '${model}_${field}'; +$config['auto_transaction'] = FALSE; +$config['auto_populate_has_many'] = FALSE; +$config['auto_populate_has_one'] = FALSE; +$config['all_array_uses_ids'] = FALSE; +// set to FALSE to use the same DB instance across the board (breaks subqueries) +// Set to any acceptable parameters to $CI->database() to override the default. +$config['db_params'] = ''; +// Uncomment to enable the production cache +// $config['production_cache'] = 'datamapper/cache'; +$config['extensions_path'] = 'datamapper'; +$config['extensions'] = array(); + +/* End of file datamapper.php */ +/* Location: ./application/config/datamapper.php */ \ No newline at end of file diff --git a/application/datamapper/array.php b/application/datamapper/array.php new file mode 100644 index 0000000..5736b2a --- /dev/null +++ b/application/datamapper/array.php @@ -0,0 +1,194 @@ +fields; + } + + $result = array(); + + foreach($fields as $f) + { + // handle related fields + if(array_key_exists($f, $object->has_one) || array_key_exists($f, $object->has_many)) + { + // each related item is stored as an array of ids + // Note: this method will NOT get() the related object. + $rels = array(); + foreach($object->{$f} as $item) + { + $rels[] = $item->id; + } + $result[$f] = $rels; + } + else + { + // just the field. + $result[$f] = $object->{$f}; + } + } + + return $result; + } + + /** + * Convert the entire $object->all array result set into an array of + * associative arrays. + * + * @see to_array + * @param DataMapper $object The DataMapper Object to convert + * @param array $fields Array of fields to include. If empty, includes all database columns. + * @return array An array of associative arrays. + */ + function all_to_array($object, $fields = '') + { + // loop through each object in the $all array, convert them to + // an array, and add them to a new array. + $result = array(); + foreach($object as $o) + { + $result[] = $o->to_array($fields); + } + return $result; + } + + /** + * Convert an associative array back into a DataMapper model. + * + * If $fields is provided, missing fields are assumed to be empty checkboxes. + * + * @param DataMapper $object The DataMapper Object to save to. + * @param array $data A an associative array of fields to convert. + * @param array $fields Array of 'safe' fields. If empty, only includes the database columns. + * @param bool $save If TRUE, then attempt to save the object automatically. + * @return array|bool A list of newly related objects, or the result of the save if $save is TRUE + */ + function from_array($object, $data, $fields = '', $save = FALSE) + { + // keep track of newly related objects + $new_related_objects = array(); + + // Assume all database columns. + // In this case, simply store $fields that are in the $data array. + if(empty($fields)) + { + $fields = $object->fields; + foreach($data as $k => $v) { + if(in_array($k, $fields)) + { + $object->{$k} = $v; + } + } + } + else + { + // If $fields is provided, assume all $fields should exist. + foreach($fields as $f) + { + if(array_key_exists($f, $object->has_one)) + { + // Store $has_one relationships + $c = get_class($object->{$f}); + $rel = new $c(); + $id = isset($data[$f]) ? $data[$f] : 0; + $rel->get_by_id($id); + if($rel->exists()) + { + // The new relationship exists, save it. + $new_related_objects[$f] = $rel; + } + else + { + // The new relationship does not exist, delete the old one. + $object->delete($object->{$f}->get()); + } + } + else if(array_key_exists($f, $object->has_many)) + { + // Store $has_many relationships + $c = get_class($object->{$f}); + $rels = new $c(); + $ids = isset($data[$f]) ? $data[$f] : FALSE; + if(empty($ids)) + { + // if no IDs were provided, delete all old relationships. + $object->delete($object->{$f}->select('id')->get()->all); + } + else + { + // Otherwise, get the new ones... + $rels->where_in('id', $ids)->select('id')->get(); + // Store them... + $new_related_objects[$f] = $rels->all; + // And delete any old ones that do not exist. + $old_rels = $object->{$f}->where_not_in('id', $ids)->select('id')->get(); + $object->delete($old_rels->all); + } + } + else + { + // Otherwise, if the $data was set, store it... + if(isset($data[$f])) + { + $v = $data[$f]; + } + else + { + // Or assume it was an unchecked checkbox, and clear it. + $v = FALSE; + } + $object->{$f} = $v; + } + } + } + if($save) + { + // Auto save + return $object->save($new_related_objects); + } + else + { + // return new objects + return $new_related_objects; + } + } + +} + +/* End of file array.php */ +/* Location: ./application/datamapper/array.php */ diff --git a/application/datamapper/csv.php b/application/datamapper/csv.php new file mode 100644 index 0000000..9e9c1f0 --- /dev/null +++ b/application/datamapper/csv.php @@ -0,0 +1,225 @@ +fields; + } + + $success = TRUE; + + // determine if we need to open the file or not. + if(is_string($filename)) + { + // open the file, if possible. + $fp = fopen($filename, 'w'); + if($fp === FALSE) + { + log_message('error', 'CSV Extension: Unable to open file ' . $filename); + return FALSE; + } + } + else + { + // assume file pointer. + $fp = $filename; + } + + if($include_header) + { + // Print out header line + $success = fputcsv($fp, $fields); + } + + if($success) + { + foreach($object as $o) + { + // convert each object into an array + $result = array(); + foreach($fields as $f) + { + $result[] = $o->{$f}; + } + // output CSV-formatted line + $success = fputcsv($fp, $result); + if(!$success) + { + // stop on first failure. + break; + } + } + } + + if(is_string($filename)) + { + fclose($fp); + } + + return $success; + } + + /** + * Import objects from a CSV file. + * + * Completely empty rows are automatically skipped, as are rows that + * start with a # sign (assumed to be comments). + * + * @param DataMapper $object The type of DataMapper Object to import + * @param mixed $filename Name of CSV file, or a file pointer. + * @param array $fields If empty, the database fields are used. Otherwise used to limit what fields are saved. + * @param boolean $header_row If true, the first line is assumed to be a header row. Defaults to true. + * @param mixed $callback A callback method for each row. Can return FALSE on failure to save, or 'stop' to stop the import. + * @return array Array of imported objects, or FALSE if unable to import. + */ + function csv_import($object, $filename, $fields = '', $header_row = TRUE, $callback = NULL) + { + $class = get_class($object); + + if(empty($fields)) + { + $fields = $object->fields; + } + + // determine if we need to open the file or not. + if(is_string($filename)) + { + // open the file, if possible. + $fp = fopen($filename, 'r'); + if($fp === FALSE) + { + log_message('error', 'CSV Extension: Unable to open file ' . $filename); + return FALSE; + } + } + else + { + // assume file pointer. + $fp = $filename; + } + + if(empty($callback)) + { + $result = array(); + } + else + { + $result = 0; + } + $columns = NULL; + + while(($data = fgetcsv($fp)) !== FALSE) + { + // get column names + if(is_null($columns)) + { + if($header_row) + { + // store header row for column names + $columns = $data; + // only include columns in $fields + foreach($columns as $index => $name) + { + if( ! in_array($name, $fields)) + { + // mark column as false to skip + $columns[$index] = FALSE; + } + } + continue; + } + else + { + $columns = $fields; + } + } + + // skip on comments and empty rows + if(empty($data) || $data[0][0] == '#' || implode('', $data) == '') + { + continue; + } + + // create the object to save + $o = new $class(); + foreach($columns as $index => $key) + { + if(count($data) <= $index) + { + // more header columns than data columns + break; + } + + // skip columns that were determined to not be needed above. + if($key === FALSE) + { + continue; + } + + // finally, it's OK to save the data column. + $o->{$key} = $data[$index]; + } + + if( empty($callback)) + { + $result[] = $o; + } + else + { + $test = call_user_func($callback, $o); + if($test === 'stop') + { + break; + } + if($test !== FALSE) + { + $result++; + } + } + } + + if(is_string($filename)) + { + fclose($fp); + } + + return $result; + } + +} + +/* End of file csv.php */ +/* Location: ./application/datamapper/csv.php */ diff --git a/application/datamapper/htmlform.php b/application/datamapper/htmlform.php new file mode 100644 index 0000000..b556ce6 --- /dev/null +++ b/application/datamapper/htmlform.php @@ -0,0 +1,793 @@ + 'integer', + 'numeric' => 'numeric', + 'is_natural' => 'natural', + 'is_natural_no_zero' => 'positive_int', + 'valid_email' => 'email', + 'valid_ip' => 'ip', + 'valid_base64' => 'base64', + 'valid_date' => 'date', + 'alpha_dash_dot' => 'alpha_dash_dot', + 'alpha_slash_dot' => 'alpha_slash_dot', + 'alpha' => 'alpha', + 'alpha_numeric' => 'alpha_numeric', + 'alpha_dash' => 'alpha_dash', + 'required' => 'required' + ); + + function __construct($options = array()) { + foreach($options as $k => $v) + { + $this->{$k} = $v; + } + $this->CI =& get_instance(); + $this->load = $this->CI->load; + } + + // -------------------------------------------------------------------------- + + /** + * Render a single field. Can be used to chain together multiple fields in a column. + * + * @param object $object The DataMapper Object to use. + * @param string $field The field to render. + * @param string $type The type of field to render. + * @param array $options Various options to modify the output. + * @return Rendered String. + */ + function render_field($object, $field, $type = NULL, $options = NULL) + { + $value = ''; + + if(array_key_exists($field, $object->has_one) || array_key_exists($field, $object->has_many)) + { + // Create a relationship field + $one = array_key_exists($field, $object->has_one); + + // attempt to look up the current value(s) + if( ! isset($options['value'])) + { + if($this->CI->input->post($field)) + { + $value = $this->CI->input->post($field); + } + else + { + // load the related object(s) + $sel = $object->{$field}->select('id')->get(); + if($one) + { + // only a single value is allowed + $value = $sel->id; + } + else + { + // save what might be multiple values + $value = array(); + foreach($sel as $s) + { + $value[] = $s->id; + } + } + } + + } + else + { + // value was already set in the options + $value = $options['value']; + unset($options['value']); + } + + // Attempt to get a list of possible values + if( ! isset($options['list']) || is_object($options['list'])) + { + if( ! isset($options['list'])) + { + // look up all of the related values + $c = get_class($object->{$field}); + $total_items = new $c; + // See if the custom method is defined + if(method_exists($total_items, 'get_htmlform_list')) + { + // Get customized list + $total_items->get_htmlform_list($object, $field); + } + else + { + // Get all items + $total_items->get_iterated(); + } + } + else + { + // process a passed-in DataMapper object + $total_items = $options['list']; + } + $list = array(); + foreach($total_items as $item) + { + // use the __toString value of the item for the label + $list[$item->id] = (string)$item; + } + $options['list'] = $list; + } + + // By if there can be multiple items, use a dropdown for large lists, + // and a set of checkboxes for a small one. + if($one || count($options['list']) > 6) + { + $default_type = 'dropdown'; + if( ! $one && ! isset($options['size'])) + { + // limit to no more than 8 items high. + $options['size'] = min(count($options['list']), 8); + } + } + else + { + $default_type = 'checkbox'; + } + } + else + { + // attempt to look up the current value(s) + if( ! isset($options['value'])) + { + if($this->CI->input->post($field)) + { + $value = $this->CI->input->post($field); + // clear default if set + unset($options['default_value']); + } + else + { + if(isset($options['default_value'])) + { + $value = $options['default_value']; + unset($options['default_value']); + } + else + { + // the field IS the value. + $value = $object->{$field}; + } + } + + } + else + { + // value was already set in the options + $value = $options['value']; + unset($options['value']); + } + // default to text + $default_type = ($field == 'id') ? 'hidden' : 'text'; + + // determine default attributes + $a = array(); + // such as the size of the field. + $max = $this->_get_validation_rule($object, $field, 'max_length'); + if($max === FALSE) + { + $max = $this->_get_validation_rule($object, $field, 'exact_length'); + } + if($max !== FALSE) + { + $a['maxlength'] = $max; + $a['size'] = min($max, 30); + } + $list = $this->_get_validation_info($object, $field, 'values', FALSE); + if($list !== FALSE) + { + $a['list'] = $list; + } + $options = $options + $a; + $extra_class = array(); + + // Add any of the known rules as classes (for JS processing) + foreach($this->auto_rule_classes as $rule => $c) + { + if($this->_get_validation_rule($object, $field, $rule) !== FALSE) + { + $extra_class[] = $c; + } + } + + // add or set the class on the field. + if( ! empty($extra_class)) + { + $extra_class = implode(' ', $extra_class); + if(isset($options['class'])) + { + $options['class'] .= ' ' . $extra_class; + } + else + { + $options['class'] = $extra_class; + } + } + } + + // determine the renderer type + $type = $this->_get_type($object, $field, $type); + if(empty($type)) + { + $type = $default_type; + } + + // attempt to find the renderer function + if(method_exists($this, '_input_' . $type)) + { + return $this->{'_input_' . $type}($object, $field, $value, $options); + } + else if(function_exists('input_' . $type)) + { + return call_user_func('input_' . $type, $object, $field, $value, $options); + } + else + { + log_message('error', 'FormMaker: Unable to find a renderer for '.$type); + return 'FormMaker: UNABLE TO FIND A RENDERER FOR '.$type.''; + } + + } + + // -------------------------------------------------------------------------- + + /** + * Render a row with a single field. If $field does not exist on + * $object->validation, then $field is output as-is. + * + * @param object $object The DataMapper Object to use. + * @param string $field The field to render (or content) + * @param string $type The type of field to render. + * @param array $options Various options to modify the output. + * @param string $row_template The template to use, or NULL to use the default. + * @return Rendered String. + */ + function render_row($object, $field, $type = NULL, $options = array(), $row_template = NULL) + { + // try to determine type automatically + $type = $this->_get_type($object, $field, $type); + + if( ! isset($object->validation[$field]) && (empty($type) || $type == 'section' || $type == 'none')) + { + // this could be a multiple-field row, or just some text. + // if $type is 'section, it will be rendered using the section template. + $error = ''; + $label = ''; + $content = $field; + $id = NULL; + } + else + { + // use validation information to render the field. + $content = $this->render_field($object, $field, $type, $options); + if(empty($row_template)) + { + if($type == 'hidden' || $field == 'id') + { + $row_template = 'none'; + } + else + { + $row_template = $this->row_template; + } + } + // determine if there is an existing error + $error = isset($object->error->{$field}) ? $object->error->{$field} : ''; + // determine if there is a pre-defined label + $label = $this->_get_validation_info($object, $field, 'label', $field); + // the field IS the id + $id = $field; + } + + $required = $this->_get_validation_rule($object, $field, 'required'); + + // Append these items. Values in $options have priority + $view_data = $options + array( + 'object' => $object, + 'content' => $content, + 'field' => $field, + 'label' => $label, + 'error' => $error, + 'id' => $id, + 'required' => $required + ); + + if(is_null($row_template)) + { + if(empty($type)) + { + $row_template = 'none'; + } + else if($type == 'section') + { + $row_template = $this->section_template; + } + else + { + $row_template = $this->row_template; + } + } + + if($row_template == 'none') + { + return $content; + } + else + { + return $this->load->view($row_template, $view_data, TRUE); + } + } + + // -------------------------------------------------------------------------- + + /** + * Renders an entire form. + * + * @param object $object The DataMapper Object to use. + * @param string $fields An associative array that defines the form. + * @param string $template The template to use. + * @param string $row_template The template to use for rows. + * @param array $template_options The template to use for rows. + * @return Rendered String. + */ + function render_form($object, $fields, $url = '', $options = array(), $template = NULL, $row_template = NULL) + { + if(empty($url)) + { + // set url to current url + $url =$this->CI->uri->uri_string(); + } + + if(is_null($template)) + { + $template = $this->form_template; + } + + $rows = ''; + foreach($fields as $field => $field_options) + { + $rows .= $this->_render_row_from_form($object, $field, $field_options, $row_template); + } + + $view_data = $options + array( + 'object' => $object, + 'fields' => $fields, + 'url' => $url, + 'rows' => $rows + ); + + return $this->load->view($template, $view_data, TRUE); + } + + // -------------------------------------------------------------------------- + // Private Methods + // -------------------------------------------------------------------------- + + // Converts information from render_form into a row of objects. + function _render_row_from_form($object, $field, $options, $row_template, $row = TRUE) + { + if(is_int($field)) + { + // simple form, or HTML-content + $field = $options; + $options = NULL; + } + if(is_null($options)) + { + // always have an array for options + $options = array(); + } + + $type = ''; + if( ! is_array($options)) + { + // if options is a single string, assume it is the type. + $type = $options; + $options = array(); + } + + if(isset($options['type'])) + { + // type was set in options + $type = $options['type']; + unset($options['type']); + } + + // see if a different row_template was in the options + $rt = $row_template; + if(isset($options['template'])) + { + $rt = $options['template']; + unset($options['template']); + } + + // Multiple fields, render them all as one. + if(is_array($field)) + { + if(isset($field['row_options'])) + { + $options = $field['row_options']; + unset($field['row_options']); + } + $ret = ''; + $sep = ' '; + if(isset($field['input_separator'])) + { + $sep = $field['input_separator']; + unset($field['input_separator']); + } + foreach($field as $f => $fo) + { + // add each field to a list + if( ! empty($ret)) + { + $ret .= $sep; + } + $ret .= $this->_render_row_from_form($object, $f, $fo, $row_template, FALSE); + } + + // renders into a row or field below. + $field = $ret; + } + if($row) + { + // if row is set, render the whole row. + return $this->render_row($object, $field, $type, $options, $rt); + } + else + { + // render just the field. + return $this->render_field($object, $field, $type, $options); + } + } + + // -------------------------------------------------------------------------- + + // Attempts to look up the field's type + function _get_type($object, $field, $type) + { + if(empty($type)) + { + $type = $this->_get_validation_info($object, $field, 'type', NULL); + } + return $type; + } + + // -------------------------------------------------------------------------- + + // Returns a field from the validation array + function _get_validation_info($object, $field, $val, $default = '') + { + if(isset($object->validation[$field][$val])) + { + return $object->validation[$field][$val]; + } + return $default; + } + + // -------------------------------------------------------------------------- + + // Returns the value (or TRUE) of the validation rule, or FALSE if it does not exist. + function _get_validation_rule($object, $field, $rule) + { + $r = $this->_get_validation_info($object, $field, 'rules', FALSE); + if($r !== FALSE) + { + if(isset($r[$rule])) + { + return $r[$rule]; + } + else if(in_array($rule, $r, TRUE)) + { + return TRUE; + } + } + return FALSE; + } + + // -------------------------------------------------------------------------- + // Input Types + // -------------------------------------------------------------------------- + + // Render a hidden input + function _input_hidden($object, $id, $value, $options) + { + return $this->_render_simple_input('hidden', $id, $value, $options); + } + + // render a single-line text input + function _input_text($object, $id, $value, $options) + { + return $this->_render_simple_input('text', $id, $value, $options); + } + + // render a password input + function _input_password($object, $id, $value, $options) + { + if(isset($options['send_value'])) + { + unset($options['send_value']); + } + else + { + $value = ''; + } + return $this->_render_simple_input('password', $id, $value, $options); + } + + // render a multiline text input + function _input_textarea($object, $id, $value, $options) + { + if(isset($options['value'])) + { + $value = $options['value']; + unset($options['value']); + } + $a = $options + array( + 'name' => $id, + 'id' => $id + ); + return $this->_render_node('textarea', $a, htmlspecialchars($value)); + } + + // render a dropdown + function _input_dropdown($object, $id, $value, $options) + { + $list = $options['list']; + unset($options['list']); + $selected = $value; + if(isset($options['value'])) + { + $selected = $options['value']; + unset($options['value']); + } + if( ! is_array($selected)) + { + $selected = array($selected); + } + else + { + // force multiple + $options['multiple'] = 'multiple'; + } + $l = $this->_options($list, $selected); + + $name = $id; + if(isset($options['multiple'])) + { + $name .= '[]'; + } + $a = $options + array( + 'name' => $name, + 'id' => $id + ); + return $this->_render_node('select', $a, $l); + } + + // used to render an options list. + function _options($list, $sel) + { + $l = ''; + foreach($list as $opt => $label) + { + if(is_array($label)) + { + $l .= ''; + $l .= $this->_options($label, $sel); + $l .= ''; + } + else + { + $a = array('value' => $opt); + if(in_array($opt, $sel)) + { + $a['selected'] = 'selected'; + } + $l .= $this->_render_node('option', $a, htmlspecialchars($label)); + } + } + return $l; + } + + // render a checkbox or series of checkboxes + function _input_checkbox($object, $id, $value, $options) + { + return $this->_checkbox('checkbox', $id, $value, $options); + } + + // render a series of radio buttons + function _input_radio($object, $id, $value, $options) + { + return $this->_checkbox('radio', $id, $value, $options); + } + + // renders one or more checkboxes or radio buttons + function _checkbox($type, $id, $value, $options, $sub_id = '', $label = '') + { + if(isset($options['value'])) + { + $value = $options['value']; + unset($options['value']); + } + // if there is a list in options, render our multiple checkboxes. + if(isset($options['list'])) + { + $list = $options['list']; + unset($options['list']); + $ret = ''; + if( ! is_array($value)) + { + if(is_null($value) || $value === FALSE || $value === '') + { + $value = array(); + } + else + { + $value = array($value); + } + } + $sep = '
'; + if(isset($options['input_separator'])) + { + $sep = $options['input_separator']; + unset($options['input_separator']); + } + foreach($list as $k => $v) + { + if( ! empty($ret)) + { + // put each node on one line. + $ret .= $sep; + } + $ret .= $this->_checkbox($type, $id, $value, $options, $k, $v); + } + return $ret; + } + else + { + // just render the single checkbox. + $node_id = $id; + if( ! empty($sub_id)) + { + // there are multiple nodes with this id, append the sub_id + $node_id .= '_' . $sub_id; + $field_value = $sub_id; + } + else + { + // sub_id is the same as the node's id + $sub_id = $id; + $field_value = '1'; + } + $name = $id; + if(is_array($value)) + { + // allow for multiple results + $name .= '[]'; + } + // node attributes + $a = $options + array( + 'type' => $type, + 'id' => $node_id, + 'name' => $name, + 'value' => $field_value + ); + // if checked wasn't overridden + if( ! isset($a['checked'])) + { + // determine if this is a multiple checkbox or not. + $checked = $value; + if(is_array($checked)) + { + $checked = in_array($sub_id, $value); + } + if($checked) + { + $a['checked'] = 'checked'; + } + } + $ret = $this->_render_node('input', $a); + if( ! empty($label)) + { + $ret .= ' ' . $this->_render_node('label', array('for' => $node_id), $label); + } + return $ret; + } + } + + // render a file upload input + function _input_file($object, $id, $value, $options) + { + $a = $options + array( + 'type' => 'file', + 'name' => $id, + 'id' => $id + ); + return $this->_render_node('input', $a); + } + + // Utility method to render a normal + function _render_simple_input($type, $id, $value, $options) + { + $a = $options + array( + 'type' => $type, + 'name' => $id, + 'id' => $id, + 'value' => $value + ); + return $this->_render_node('input', $a); + } + + // Utility method to render a node. + function _render_node($type, $attributes, $content = FALSE) + { + // generate node + $res = '<' . $type; + foreach($attributes as $att => $v) + { + // the special attribute '_' is rendered directly. + if($att == '_') + { + $res .= ' ' . $v; + } + else + { + if($att != 'label') + { + $res .= ' ' . $att . '="' . htmlspecialchars((string)$v) . '"'; + } + } + } + // allow for content-containing nodes + if($content !== FALSE) + { + $res .= '>' . $content . ''; + } + else + { + $res .= ' />'; + } + return $res; + } + +} + +/* End of file htmlform.php */ +/* Location: ./application/datamapper/htmlform.php */ \ No newline at end of file diff --git a/application/datamapper/json.php b/application/datamapper/json.php new file mode 100644 index 0000000..e41d6e9 --- /dev/null +++ b/application/datamapper/json.php @@ -0,0 +1,220 @@ +fields; + } + $result = array(); + foreach($fields as $f) + { + $result[$f] = $object->{$f}; + } + $json = json_encode($result); + if($json === FALSE) + { + return FALSE; + } + if($pretty_print) + { + $json = $this->_json_format($json); + } + return $json; + } + + /** + * Convert the entire $object->all array result set into JSON code. + * + * @param DataMapper $object The DataMapper Object to convert + * @param array $fields Array of fields to include. If empty, includes all database columns. + * @param boolean $pretty_print Format the JSON code for legibility. + * @return string A JSON formatted String, or FALSE if an error occurs. + */ + public function all_to_json($object, $fields = '', $pretty_print = FALSE) + { + if(empty($fields)) + { + $fields = $object->fields; + } + $result = array(); + foreach($object as $o) + { + $temp = array(); + foreach($fields as $f) + { + $temp[$f] = $o->{$f}; + } + $result[] = $temp; + } + $json = json_encode($result); + if($json === FALSE) + { + return FALSE; + } + if($pretty_print) + { + $json = $this->_json_format($json); + } + return $json; + } + + /** + * Convert a JSON object back into a DataMapper model. + * + * @param DataMapper $object The DataMapper Object to save to. + * @param string $json_code A string that contains JSON code. + * @param array $fields Array of 'safe' fields. If empty, only include the database columns. + * @return bool TRUE or FALSE on success or failure of converting the JSON string. + */ + public function from_json($object, $json_code, $fields = '') + { + if(empty($fields)) + { + $fields = $object->fields; + } + $data = json_decode($json_code); + if($data === FALSE) + { + return FALSE; + } + foreach($data as $k => $v) { + if(in_array($k, $fields)) + { + $object->{$k} = $v; + } + } + return TRUE; + } + + /** + * Sets the HTTP Content-Type header to application/json + * + * @param DataMapper $object + */ + public function set_json_content_type($object) + { + $CI =& get_instance(); + $CI->output->set_header('Content-Type: application/json'); + } + + /** + * Formats a JSON string for readability. + * + * From @link http://php.net/manual/en/function.json-encode.php + * + * @param string $json Unformatted JSON + * @return string Formatted JSON + */ + private function _json_format($json) + { + $tab = " "; + $new_json = ""; + $indent_level = 0; + $in_string = false; + + $json_obj = json_decode($json); + + if($json_obj === false) + return false; + + $json = json_encode($json_obj); + $len = strlen($json); + + for($c = 0; $c < $len; $c++) + { + $char = $json[$c]; + switch($char) + { + case '{': + case '[': + if(!$in_string) + { + $new_json .= $char . "\n" . str_repeat($tab, $indent_level+1); + $indent_level++; + } + else + { + $new_json .= $char; + } + break; + case '}': + case ']': + if(!$in_string) + { + $indent_level--; + $new_json .= "\n" . str_repeat($tab, $indent_level) . $char; + } + else + { + $new_json .= $char; + } + break; + case ',': + if(!$in_string) + { + $new_json .= ",\n" . str_repeat($tab, $indent_level); + } + else + { + $new_json .= $char; + } + break; + case ':': + if(!$in_string) + { + $new_json .= ": "; + } + else + { + $new_json .= $char; + } + break; + case '"': + if($c > 0 && $json[$c-1] != '\\') + { + $in_string = !$in_string; + } + default: + $new_json .= $char; + break; + } + } + + return $new_json; + } + +} + +/* End of file json.php */ +/* Location: ./application/datamapper/json.php */ diff --git a/application/datamapper/nestedsets.php b/application/datamapper/nestedsets.php new file mode 100644 index 0000000..ec4ed40 --- /dev/null +++ b/application/datamapper/nestedsets.php @@ -0,0 +1,1369 @@ +tree_config($object, $options); + } + } + + // ----------------------------------------------------------------- + + /** + * runtime configuration of this nestedsets tree + * + * @param object the DataMapper object + * @param mixed optional, array of options or NULL + * @return object the updated DataMapper object + * @access public + */ + function tree_config($object, $options = array() ) + { + // make sure the load-time options parameter is an array + if ( ! is_array($options) ) + { + $options = array(); + } + + // make sure the model options parameter is an array + if ( ! isset($object->nestedsets) OR ! is_array($object->nestedsets) ) + { + $object->nestedsets = array(); + } + + // loop through all options + foreach( array( $object->nestedsets, $options ) as $optarray ) + { + foreach( $optarray as $key => $value ) + { + switch ( $key ) + { + case 'name': + $this->_nodename = (string) $value; + break; + case 'symlink': + $this->_symlinkindex = (string) $value; + break; + case 'left': + $this->_leftindex = (string) $value; + break; + case 'right': + $this->_rightindex = (string) $value; + break; + case 'root': + $this->_rootfield = (string) $value; + break; + case 'value': + $this->_rootindex = (int) $value; + break; + case 'follow': + $this->use_symlink_pointers = (bool) $value; + break; + default: + break; + } + } + } + } + + // ----------------------------------------------------------------- + + /** + * select a specific root if the table contains multiple trees + * + * @param object the DataMapper object + * @return object the updated DataMapper object + * @access public + */ + function select_root($object, $tree = NULL) + { + // set the filter value + $this->_rootindex = $tree; + + // return the object + return $object; + } + + // ----------------------------------------------------------------- + // Tree constructors + // ----------------------------------------------------------------- + + /** + * create a new tree root + * + * @param object the DataMapper object + * @return object the updated DataMapper object + * @access public + */ + function new_root($object) + { + // set the pointers for the root object + $object->id = NULL; + $object->{$this->_leftindex} = 1; + $object->{$this->_rightindex} = 2; + + // add a root index if needed + if ( in_array($this->_rootfield, $object->fields) && ! is_null($this->_rootindex) ) + { + $object->{$this->_rootfield} = $this->_rootindex; + } + + // create the new tree root, and return the updated object + return $this->_insertNew($object); + } + + // ----------------------------------------------------------------- + + /** + * creates a new first child of 'node' + * + * @param object the DataMapper object + * @param object the parent node + * @return object the updated DataMapper object + * @access public + */ + function new_first_child($object, $node = NULL) + { + // a node passed? + if ( is_null($node) ) + { + // no, use the object itself + $node = $object->get_clone(); + } + + // we need a valid node for this to work + if ( ! $node->exists() ) + { + return $node; + } + + // set the pointers for the root object + $object->id = NULL; + $object->{$this->_leftindex} = $node->{$this->_leftindex} + 1; + $object->{$this->_rightindex} = $node->{$this->_leftindex} + 2; + + // add a root index if needed + if ( in_array($this->_rootfield, $object->fields) && ! is_null($this->_rootindex) ) + { + $object->{$this->_rootfield} = $this->_rootindex; + } + + // shift nodes to make room for the new child + $this->_shiftRLValues($node, $object->{$this->_leftindex}, 2); + + // create the new tree node, and return the updated object + return $this->_insertNew($object); + } + + // ----------------------------------------------------------------- + + /** + * creates a new last child of 'node' + * + * @param object the DataMapper object + * @param object the parent node + * @return object the updated DataMapper object + * @access public + */ + function new_last_child($object, $node = NULL) + { + // a node passed? + if ( is_null($node) ) + { + // no, use the object itself + $node = $object->get_clone(); + } + + // we need a valid node for this to work + if ( ! $node->exists() ) + { + return $node; + } + + // set the pointers for the root object + $object->id = NULL; + $object->{$this->_leftindex} = $node->{$this->_rightindex}; + $object->{$this->_rightindex} = $node->{$this->_rightindex} + 1; + + // add a root index if needed + if ( in_array($this->_rootfield, $object->fields) && ! is_null($this->_rootindex) ) + { + $object->{$this->_rootfield} = $this->_rootindex; + } + + // shift nodes to make room for the new child + $this->_shiftRLValues($node, $object->{$this->_leftindex}, 2); + + // create the new tree node, and return the updated object + return $this->_insertNew($object); + } + + // ----------------------------------------------------------------- + + /** + * creates a new sibling before 'node' + * + * @param object the DataMapper object + * @param object the sibling node + * @return object the updated DataMapper object + * @access public + */ + function new_previous_sibling($object, $node = NULL) + { + // a node passed? + if ( is_null($node) ) + { + // no, use the object itself + $node = $object->get_clone(); + } + + // we need a valid node for this to work + if ( ! $node->exists() ) + { + return $node; + } + + // set the pointers for the root object + $object->id = NULL; + $object->{$this->_leftindex} = $node->{$this->_leftindex}; + $object->{$this->_rightindex} = $node->{$this->_leftindex} + 1; + + // add a root index if needed + if ( in_array($this->_rootfield, $object->fields) && ! is_null($this->_rootindex) ) + { + $object->{$this->_rootfield} = $this->_rootindex; + } + + // shift nodes to make room for the new sibling + $this->_shiftRLValues($node, $object->{$this->_leftindex}, 2); + + // create the new tree node, and return the updated object + return $this->_insertNew($object); + } + + // ----------------------------------------------------------------- + + /** + * creates a new sibling after 'node' + * + * @param object the DataMapper object + * @param object the sibling node + * @return object the updated DataMapper object + * @access public + */ + function new_next_sibling($object, $node = NULL) + { + // a node passed? + if ( is_null($node) ) + { + // no, use the object itself + $node = $object->get_clone(); + } + + // we need a valid node for this to work + if ( ! $node->exists() ) + { + return $node; + } + + // set the pointers for the root object + $object->id = NULL; + $object->{$this->_leftindex} = $node->{$this->_rightindex} + 1; + $object->{$this->_rightindex} = $node->{$this->_rightindex} + 2; + + // add a root index if needed + if ( in_array($this->_rootfield, $object->fields) && ! is_null($this->_rootindex) ) + { + $object->{$this->_rootfield} = $this->_rootindex; + } + + // shift nodes to make room for the new sibling + $this->_shiftRLValues($node, $object->{$this->_leftindex}, 2); + + // create the new tree node, and return the updated object + return $this->_insertNew($object); + } + + // ----------------------------------------------------------------- + // Tree queries + // ----------------------------------------------------------------- + + + /** + * returns the root of the (selected) tree + * + * @param object the DataMapper object + * @return object the updated DataMapper object + * @access public + */ + function get_root($object) + { + // add a root index if needed + if ( in_array($this->_rootfield, $object->fields) && ! is_null($this->_rootindex) ) + { + $object->db->where($this->_rootfield, $this->_rootindex); + } + + // get the tree's root node + return $object->where($this->_leftindex, 1)->get(); + } + + // ----------------------------------------------------------------- + + /** + * returns the parent of the child 'node' + * + * @param object the DataMapper object + * @param object the child node + * @return object the updated DataMapper object + * @access public + */ + function get_parent($object, $node = NULL) + { + // a node passed? + if ( is_null($node) ) + { + // no, use the object itself + $node =& $object; + } + + // we need a valid node for this to work + if ( ! $node->exists() ) + { + return $node; + } + + // add a root index if needed + if ( in_array($this->_rootfield, $object->fields) && ! is_null($this->_rootindex) ) + { + $object->db->where($this->_rootfield, $this->_rootindex); + } + + // get the node's parent node + $object->where($this->_leftindex . ' <', $node->{$this->_leftindex}); + $object->where($this->_rightindex . ' >', $node->{$this->_rightindex}); + return $object->order_by($this->_rightindex, 'asc')->limit(1)->get(); + } + + // ----------------------------------------------------------------- + + /** + * returns the node with the requested left index pointer + * + * @param object the DataMapper object + * @param integer a node's left index value + * @return object the updated DataMapper object + * @access public + */ + function get_node_where_left($object, $left_id) + { + // add a root index if needed + if ( in_array($this->_rootfield, $object->fields) && ! is_null($this->_rootindex) ) + { + $object->db->where($this->_rootfield, $this->_rootindex); + } + + // get the node's parent node + $object->where($this->_leftindex, $left_id); + return $object->get(); + } + + // ----------------------------------------------------------------- + + /** + * returns the node with the requested right index pointer + * + * @param object the DataMapper object + * @param integer a node's right index value + * @return object the updated DataMapper object + * @access public + */ + function get_node_where_right($object, $right_id) + { + // add a root index if needed + if ( in_array($this->_rootfield, $object->fields) && ! is_null($this->_rootindex) ) + { + $object->db->where($this->_rootfield, $this->_rootindex); + } + + // get the node's parent node + $object->where($this->_rightindex, $right_id); + return $object->get(); + } + + // ----------------------------------------------------------------- + + /** + * returns the first child of the given node + * + * @param object the DataMapper object + * @param object the parent node + * @return object the updated DataMapper object + * @access public + */ + function get_first_child($object, $node = NULL) + { + // a node passed? + if ( is_null($node) ) + { + // no, use the object itself + $node =& $object; + } + + // we need a valid node for this to work + if ( ! $node->exists() ) + { + return $node; + } + + // add a root index if needed + if ( in_array($this->_rootfield, $object->fields) && ! is_null($this->_rootindex) ) + { + $object->db->where($this->_rootfield, $this->_rootindex); + } + + // get the node's first child node + $object->where($this->_leftindex, $node->{$this->_leftindex}+1); + return $object->get(); + } + + // ----------------------------------------------------------------- + + /** + * returns the last child of the given node + * + * @param object the DataMapper object + * @param object the parent node + * @return object the updated DataMapper object + * @access public + */ + function get_last_child($object, $node = NULL) + { + // a node passed? + if ( is_null($node) ) + { + // no, use the object itself + $node =& $object; + } + + // we need a valid node for this to work + if ( ! $node->exists() ) + { + return $node; + } + + // add a root index if needed + if ( in_array($this->_rootfield, $object->fields) && ! is_null($this->_rootindex) ) + { + $object->db->where($this->_rootfield, $this->_rootindex); + } + + // get the node's last child node + $object->where($this->_rightindex, $node->{$this->_rightindex}-1); + return $object->get(); + } + + // ----------------------------------------------------------------- + + /** + * returns the previous sibling of the given node + * + * @param object the DataMapper object + * @param object the sibling node + * @return object the updated DataMapper object + * @access public + */ + function get_previous_sibling($object, $node = NULL) + { + // a node passed? + if ( is_null($node) ) + { + // no, use the object itself + $node =& $object; + } + + // we need a valid node for this to work + if ( ! $node->exists() ) + { + return $node; + } + + // add a root index if needed + if ( in_array($this->_rootfield, $object->fields) && ! is_null($this->_rootindex) ) + { + $object->db->where($this->_rootfield, $this->_rootindex); + } + + // get the node's previous sibling node + $object->where($this->_rightindex, $node->{$this->_leftindex}-1); + return $object->get(); + } + + // ----------------------------------------------------------------- + + /** + * returns the next sibling of the given node + * + * @param object the DataMapper object + * @param object the sibling node + * @return object the updated DataMapper object + * @access public + */ + function get_next_sibling($object, $node = NULL) + { + // a node passed? + if ( is_null($node) ) + { + // no, use the object itself + $node =& $object; + } + + // we need a valid node for this to work + if ( ! $node->exists() ) + { + return $node; + } + + // add a root index if needed + if ( in_array($this->_rootfield, $object->fields) && ! is_null($this->_rootindex) ) + { + $object->db->where($this->_rootfield, $this->_rootindex); + } + + // get the node's next sibling node + $object->where($this->_leftindex, $node->{$this->_rightindex}+1); + return $object->get(); + } + + // ----------------------------------------------------------------- + // Boolean tree functions + // ----------------------------------------------------------------- + + /** + * check if the object is a valid tree node + * + * @param object the DataMapper object + * @return boolean + * @access public + */ + function is_valid_node($object) + { + if ( ! $object->exists() ) + { + return FALSE; + } + elseif ( ! isset($object->{$this->_leftindex}) OR ! is_numeric($object->{$this->_leftindex}) OR $object->{$this->_leftindex} <=0 ) + { + return FALSE; + } + elseif ( ! isset($object->{$this->_rightindex}) OR ! is_numeric($object->{$this->_rightindex}) OR $object->{$this->_rightindex} <=0 ) + { + return FALSE; + } + elseif ( $object->{$this->_leftindex} >= $object->{$this->_rightindex} ) + { + return FALSE; + } + elseif ( ! empty($this->_rootfield) && ! in_array($this->_rootfield, $object->fields) ) + { + return FALSE; + } + elseif ( ! empty($this->_rootfield) && ( ! is_numeric($object->{$this->_rootfield}) OR $object->{$this->_rootfield} <=0 ) ) + { + return FALSE; + } + + // all looks well... + return TRUE; + } + + // ----------------------------------------------------------------- + + /** + * check if the object is a tree root + * + * @param object the DataMapper object + * @return boolean + * @access public + */ + function is_root($object) + { + return ( $object->exists() && $this->is_valid_node($object) && $object->{$this->_leftindex} == 1 ); + } + + // ----------------------------------------------------------------- + + /** + * check if the object is a tree leaf (node with no children) + * + * @param object the DataMapper object + * @return boolean + * @access public + */ + function is_leaf($object) + { + return ( $object->exists() && $this->is_valid_node($object) && $object->{$this->_rightindex} - $object->{$this->_leftindex} == 1 ); + } + + // ----------------------------------------------------------------- + + /** + * check if the object is a child node + * + * @param object the DataMapper object + * @return boolean + * @access public + */ + function is_child($object) + { + return ( $object->exists() && $this->is_valid_node($object) && $object->{$this->_leftindex} > 1 ); + } + + // ----------------------------------------------------------------- + + /** + * check if the object is a child of node + * + * @param object the DataMapper object + * @param object the parent node + * @return boolean + * @access public + */ + function is_child_of($object, $node = NULL) + { + // validate the objects + if ( ! $this->is_valid_node($object) OR ! $this->is_valid_node($node) ) + { + return FALSE; + } + + return ( $object->{$this->_leftindex} > $node->{$this->_leftindex} && $object->{$this->_rightindex} < $node->{$this->_rightindex} ); + } + + // ----------------------------------------------------------------- + + /** + * check if the object is the parent of node + * + * @param object the DataMapper object + * @param object the parent node + * @return boolean + * @access public + */ + function is_parent_of($object, $node = NULL) + { + // validate the objects + if ( ! $this->is_valid_node($object) OR ! $this->is_valid_node($node) ) + { + return FALSE; + } + + // fetch the parent of our child node + $parent = $node->get_clone()->get_parent(); + + return ( $parent->id === $object->id ); + } + + // ----------------------------------------------------------------- + + /** + * check if the object has a parent + * + * Note: this is an alias for is_child() + * + * @param object the DataMapper object + * @return boolean + * @access public + */ + function has_parent($object) + { + return $this->is_child($object); + } + + // ----------------------------------------------------------------- + + /** + * check if the object has children + * + * Note: this is an alias for ! is_leaf() + * + * @param object the DataMapper object + * @return boolean + * @access public + */ + function has_children($object) + { + return $this->is_leaf($object) ? FALSE : TRUE; + } + + // ----------------------------------------------------------------- + + /** + * check if the object has a previous silbling + * + * @param object the DataMapper object + * @return boolean + * @access public + */ + function has_previous_sibling($object) + { + // fetch the result using a clone + $node = $object->get_clone(); + return $this->is_valid_node($node->get_previous_sibling($object)); + } + + // ----------------------------------------------------------------- + + /** + * check if the object has a next silbling + * + * @param object the DataMapper object + * @return boolean + * @access public + */ + function has_next_sibling($object) + { + // fetch the result using a clone + $node = $object->get_clone(); + return $this->is_valid_node($node->get_next_sibling($object)); + } + + // ----------------------------------------------------------------- + // Integer tree functions + // ----------------------------------------------------------------- + + /** + * return the count of the objects children + * + * @param object the DataMapper object + * @return integer + * @access public + */ + function count_children($object) + { + return ( $object->exists() ? (($object->{$this->_rightindex} - $object->{$this->_leftindex} - 1) / 2) : FALSE ); + } + + // ----------------------------------------------------------------- + + /** + * return the node level, where the root = 0 + * + * @param object the DataMapper object + * @return mixed integer, of FALSE in case no valid object was passed + * @access public + */ + function level($object) + { + if ( $object->exists() ) + { + // add a root index if needed + if ( in_array($this->_rootfield, $object->fields) && ! is_null($this->_rootindex) ) + { + $object->db->where($this->_rootfield, $this->_rootindex); + } + + $object->where($this->_leftindex.' <', $object->{$this->_leftindex}); + $object->where($this->_rightindex.' >', $object->{$this->_rightindex}); + return $object->count(); + } + else + { + return FALSE; + } + } + + // ----------------------------------------------------------------- + // Tree reorganisation + // ----------------------------------------------------------------- + + /** + * move the object as next sibling of 'node' + * + * @param object the DataMapper object + * @param object the sibling node + * @return object the updated DataMapper object + * @access public + */ + function make_next_sibling_of($object, $node) + { + return $this->_moveSubtree($object, $node, $node->{$this->_rightindex}+1); + } + + // ----------------------------------------------------------------- + + /** + * move the object as previous sibling of 'node' + * + * @param object the DataMapper object + * @param object the sibling node + * @return object the updated DataMapper object + * @access public + */ + function make_previous_sibling_of($object, $node) + { + return $this->_moveSubtree($object, $node, $node->{$this->_leftindex}); + } + + // ----------------------------------------------------------------- + + /** + * move the object as first child of 'node' + * + * @param object the DataMapper object + * @param object the sibling node + * @return object the updated DataMapper object + * @access public + */ + function make_first_child_of($object, $node) + { + return $this->_moveSubtree($object, $node, $node->{$this->_leftindex}+1); + } + + // ----------------------------------------------------------------- + + /** + * move the object as last child of 'node' + * + * @param object the DataMapper object + * @param object the sibling node + * @return object the updated DataMapper object + * @access public + */ + function make_last_child_of($object, $node) + { + return $this->_moveSubtree($object, $node, $node->{$this->_rightindex}); + } + + // ----------------------------------------------------------------- + // Tree destructors + // ----------------------------------------------------------------- + + /** + * deletes the entire tree structure including all records + * + * @param object the DataMapper object + * @param mixed optional, id of the tree to delete + * @return object the updated DataMapper object + * @access public + */ + function remove_tree($object, $tree_id = NULL) + { + // if we have multiple roots + if ( in_array($this->_rootfield, $object->fields) ) + { + // was a tree id passed? + if ( ! is_null($tree_id) ) + { + // only delete the selected one + $object->db->where($this->_rootfield, $tree_id)->delete($object->table); + } + elseif ( ! is_null($this->_rootindex) ) + { + // only delete the selected one + $object->db->where($this->_rootfield, $this->_rootindex)->delete($object->table); + } + else + { + // delete them all + $object->db->truncate($object->table); + } + } + else + { + // delete them all + $object->db->truncate($object->table); + } + + // return the cleared object + return $object->clear(); + } + + // ----------------------------------------------------------------- + + /** + * deletes the current object, and all childeren + * + * @param object the DataMapper object + * @return object the updated DataMapper object + * @access public + */ + function remove_node($object) + { + // we need a valid node to do this + if ( $object->exists() ) + { + // if we have multiple roots + if ( in_array($this->_rootfield, $object->fields) && ! is_null($this->_rootindex) ) + { + // only delete the selected one + $object->db->where($this->_rootfield, $this->_rootindex); + } + + // clone the object, we need to it shift later + $clone = $object->get_clone(); + + // select the node and all children + $object->db->where($this->_leftindex . ' >=', $object->{$this->_leftindex}); + $object->db->where($this->_rightindex . ' <=', $object->{$this->_rightindex}); + + // delete them all + $object->db->delete($object->table); + + // re-index the tree + $this->_shiftRLValues($clone, $object->{$this->_rightindex} + 1, $clone->{$this->_leftindex} - $object->{$this->_rightindex} -1); + } + + // return the cleared object + return $object->clear(); + } + + // ----------------------------------------------------------------- + // dump methods + // ----------------------------------------------------------------- + + /** + * returns the tree in a key-value format suitable for html dropdowns + * + * @param object the DataMapper object + * @param string optional, name of the column to use + * @param boolean if true, the object itself (root of the dump) will not be included + * @return array + * @access public + */ + public function dump_dropdown($object, $field = FALSE, $skip_root = TRUE) + { + // check if a specific field has been requested + if ( empty($field) OR ! isset($this->fields[$field]) ) + { + // no field given, check if a generic name is defined + if ( ! empty($this->_nodename) ) + { + // yes, so use it + $field = $this->_nodename; + } + else + { + // can't continue without a name + return FALSE; + } + } + + // fetch the tree as an array + $tree = $this->dump_tree($object, NULL, 'array', $skip_root); + + // storage for the result + $result = array(); + + if ( $tree ) + { + // loop trough the tree + foreach ( $tree as $key => $value ) + { + $result[$value['__id']] = str_repeat(' ', ($value['__level']) * 3) . ($value['__level'] ? '» ' : '') . $value[$field]; + } + } + + // return the result + return $result; + } + + // ----------------------------------------------------------------- + + /** + * dumps the entire tree in HTML or TAB formatted output + * + * @param object the DataMapper object + * @param array list of columns to include in the dump + * @param string type of output requested, possible values 'html', 'tab', 'csv', 'array' ('array' = default) + * @param boolean if true, the object itself (root of the dump) will not be included + * @return mixed + * @access public + */ + public function dump_tree($object, $attributes = NULL, $type = 'array', $skip_root = TRUE) + { + if ( $this->is_valid_node($object) ) + { + // do we need a sub-selection of attributes? + if ( is_array($attributes) ) + { + // make sure required fields are present + $fields = array_merge($attributes, array('id', $this->_leftindex, $this->_rightindex)); + if ( ! empty($this->_nodename) && ! isset($fields[$this->_nodename] ) ) + { + $fields[] = $this->_nodename; + } + // add a select + $object->db->select($fields); + } + + // create the where clause for this query + if ( $skip_root === TRUE ) + { + // select only all children + $object->db->where($this->_leftindex . ' >', $object->{$this->_leftindex}); + $object->db->where($this->_rightindex . ' <', $object->{$this->_rightindex}); + $level = -1; + } + else + { + // select the node and all children + $object->db->where($this->_leftindex . ' >=', $object->{$this->_leftindex}); + $object->db->where($this->_rightindex . ' <=', $object->{$this->_rightindex}); + $level = -2; + } + + // if we have multiple roots + if ( in_array($this->_rootfield, $object->fields) && ! is_null($this->_rootindex) ) + { + // only delete the selected one + $object->db->where($this->_rootfield, $this->_rootindex); + } + + // fetch the result + $result = $object->db->order_by($this->_leftindex)->get($object->table)->result_array(); + + // store the last left pointer + $last_left = $object->{$this->_leftindex}; + + // create the path + if ( ! empty($this->_nodename) ) + { + $path = array( $object->{$this->_nodename} ); + } + else + { + $path = array(); + } + + // add level and path to the result + foreach ( $result as $key => $value ) + { + // for now, just store the ID + $result[$key]['__id'] = $value['id']; + + // calculate the nest level of this node + $level += $last_left - $value[$this->_leftindex] + 2; + $last_left = $value[$this->_leftindex]; + $result[$key]['__level'] = $level; + + // create the relative path to this node + $result[$key]['__path'] = ''; + if ( ! empty($this->_nodename) ) + { + $path[$level] = $value[$this->_nodename]; + for ( $i = 0; $i <= $level; $i++ ) + { + $result[$key]['__path'] .= '/' . $path[$i]; + } + } + } + + // convert the result to output + if ( in_array($type, array('tab', 'csv', 'html')) ) + { + // storage for the result + $convert = ''; + + // loop through the elements + foreach ( $result as $key => $value ) + { + // prefix based on requested type + switch ($type) + { + case 'tab'; + $convert .= str_repeat("\t", $value['__level'] * 4 ); + break; + case 'csv'; + break; + case 'html'; + $convert .= str_repeat(" ", $value['__level'] * 4 ); + break; + } + + // print the attributes requested + if ( ! is_null($attributes) ) + { + $att = reset($attributes); + while($att){ + if ( is_numeric($value[$att]) ) + { + $convert .= $value[$att]; + } + else + { + $convert .= '"'.$value[$att].'"'; + } + $att = next($attributes); + if ($att) + { + $convert .= ($type == 'csv' ? "," : " "); + } + } + } + + // postfix based on requested type + switch ($type) + { + case 'tab'; + $convert .= "\n"; + break; + case 'csv'; + $convert .= "\n"; + break; + case 'html'; + $convert .= "
"; + break; + } + } + return $convert; + } + else + { + return $result; + } + } + + return FALSE; + } + + // ----------------------------------------------------------------- + // internal methods + // ----------------------------------------------------------------- + + /** + * makes room for a new node (or nodes) by shifting the left and right + * id's of nodes with larger values than our object by $delta + * + * note that $delta can also be negative! + * + * @param object the DataMapper object + * @param integer left value of the start node + * @param integer number of positions to shift + * @return object the updated DataMapper object + * @access private + */ + private function _shiftRLValues($object, $first, $delta) + { + // we need a valid object + if ( $object->exists() ) + { + // if we have multiple roots + if ( in_array($this->_rootfield, $object->fields) && ! is_null($this->_rootindex) ) + { + // select the correct one + $object->where($this->_rootfield, $this->_rootindex); + } + + // select the range + $object->where($this->_leftindex.' >=', $first); + $object->update(array($this->_leftindex => $this->_leftindex.' + '.$delta), FALSE); + + // if we have multiple roots + if ( in_array($this->_rootfield, $object->fields) && ! is_null($this->_rootindex) ) + { + // select the correct one + $object->where($this->_rootfield, $this->_rootindex); + } + + // select the range + $object->where($this->_rightindex.' >=', $first); + $object->update(array($this->_rightindex => $this->_rightindex.' + '.$delta), FALSE); + } + + // return the object + return $object; + } + + // ----------------------------------------------------------------- + + /** + * shifts a range of nodes up or down the left and right index by $delta + * + * note that $delta can also be negative! + * + * @param object the DataMapper object + * @param integer left value of the start node + * @param integer right value of the end node + * @param integer number of positions to shift + * @return object the updated DataMapper object + * @access private + */ + private function _shiftRLRange($object, $first, $last, $delta) + { + // we need a valid object + if ( $object->exists() ) + { + // if we have multiple roots + if ( in_array($this->_rootfield, $object->fields) && ! is_null($this->_rootindex) ) + { + // select the correct one + $object->where($this->_rootfield, $this->_rootindex); + } + + // select the range + $object->where($this->_leftindex.' >=', $first); + $object->where($this->_rightindex.' <=', $last); + $object->update(array($this->_leftindex => $this->_leftindex.' + '.$delta, $this->_rightindex => $this->_rightindex.' + '.$delta), FALSE); + } + + // return the object + return $object; + } + + // ----------------------------------------------------------------- + + /** + * inserts a new record into the tree + * + * @param object the DataMapper object + * @return object the updated DataMapper object + * @access private + */ + private function _insertNew($object) + { + // for now, just save the object + $object->save(); + + // return the object + return $object; + } + + // ----------------------------------------------------------------- + + /** + * move a section of the tree to another location within the tree + * + * @param object the DataMapper object we're going to move + * @param integer the destination node's left id value + * @return object the updated DataMapper object + * @access private + */ + private function _moveSubtree($object, $node, $destination_id) + { + // if we have multiple roots + if ( in_array($this->_rootfield, $object->fields) ) + { + // make sure both nodes are part of the same tree + if ( $object->{$this->_rootfield} != $node->{$this->_rootfield} ) + { + return FALSE; + } + } + + // determine the size of the tree to move + $treesize = $object->{$this->_rightindex} - $object->{$this->_leftindex} + 1; + + // get the objects left- and right pointers + $left_id = $object->{$this->_leftindex}; + $right_id = $object->{$this->_rightindex}; + + // shift to make some space + $this->_shiftRLValues($node, $destination_id, $treesize); + + // enough room now, start the move + $this->_shiftRLRange($node, $left_id, $right_id, $destination_id - $left_id); + + // and correct index values after the source + $this->_shiftRLValues($object, $right_id + 1, $treesize * -1); + + // return the object + return $object->get_by_id($object->id); + } + +} + +/* End of file nestedsets.php */ +/* Location: ./application/datamapper/nestedsets.php */ diff --git a/application/datamapper/rowindex.php b/application/datamapper/rowindex.php new file mode 100644 index 0000000..c2cba92 --- /dev/null +++ b/application/datamapper/rowindex.php @@ -0,0 +1,211 @@ + $distinct_on If TRUE, use DISTINCT ON (not all DBs support this) + * @return bool|int Returns the index of the item, or FALSE if none are found. + */ + public function row_index($object, $id, $leave_select = array(), $distinct_on = FALSE) { + $this->first_only = TRUE; + $result = $this->get_rowindices($object, $id, $leave_select, $distinct_on); + $this->first_only = FALSE; + if(empty($result)) { + return FALSE; + } else { + reset($result); + return key($result); + } + } + + /** + * Given an already-built query and an object's ID, determine what row + * that object has in the query. + * + * @param DataMapper $object THe DataMapper object. + * @param DataMapper|array|int $id The ID or object to look for. + * @param array $leave_select A list of items to leave in the selection array, overriding the automatic removal. + * @param bool $distinct_on If TRUE, use DISTINCT ON (not all DBs support this) + * @return array Returns an array of row indices. + */ + public function row_indices($object, $ids, $leave_select = array(), $distinct_on = FALSE) { + $row_indices = array(); + if(!is_array($ids)) { + $ids = array($ids); + } + $new_ids = array(); + foreach($ids as $id) { + if(is_object($id)) { + $new_ids[] = $id->id; + } else { + $new_ids[] = intval($id); + } + } + if(!is_array($leave_select)) { + $leave_select = array(); + } + // duplicate to ensure the query isn't wiped out + $object = $object->get_clone(TRUE); + // remove the unecessary columns + $sort_columns = $this->_orderlist($object->db->ar_orderby); + $ar_select = array(); + if(empty($sort_columns) && empty($leave_select)) { + // no sort columns, so just wipe it out. + $object->db->ar_select = NULL; + } else { + // loop through the ar_select, and remove columns that + // are not specified by sorting + $select = $this->_splitselect(implode(', ', $object->db->ar_select)); + // find all aliases (they are all we care about) + foreach($select as $alias => $sel) { + if(in_array($alias, $sort_columns) || in_array($alias, $leave_select)) { + $ar_select[] = $sel; + } + } + $object->db->ar_select = NULL; + } + + if($distinct_on) { + // to ensure unique items we must DISTINCT ON the same list as the ORDER BY list. + $distinct = 'DISTINCT ON (' . preg_replace("/\s+(asc|desc)/i", "", implode(",", $object->db->ar_orderby)) . ') '; + + // add in the DISTINCT ON and the $table.id column. The FALSE prevents the items from being escaped + $object->select($distinct . $object->table.'.id', FALSE); + } else { + $object->select('id'); + } + // this ensures that the DISTINCT ON is first, since it must be + $object->db->ar_select = array_merge($object->db->ar_select, $ar_select); + + // run the query + $query = $object->get_raw(); + foreach($query->result() as $index => $row) { + $id = intval($row->id); + if(in_array($id, $new_ids)) { + $row_indices[$index] = $id; + if($this->first_only) { + break; + } + } + } + + // in case the user wants to know + $object->rowindex_total_rows = $query->num_rows(); + + // return results + return $row_indices; + } + + /** + * Processes the order_by array, and converts it into a list + * of non-fully-qualified columns. These might be aliases. + * + * @param array $order_by Original order_by array + * @return array Modified array. + */ + private function _orderlist($order_by) { + $list = array(); + $impt_parts_regex = '/([\w]+)([^\(]|$)/'; + foreach($order_by as $order_by_string) { + $parts = explode(',', $order_by_string); + foreach($parts as $part) { + // remove optional order marker + $part = preg_replace('/\s+(ASC|DESC)$/i', '', $part); + // remove all functions (might not work well on recursive) + $replacements = 1; + while($replacements > 0) { + $part = preg_replace('/[a-z][\w]*\((.*)\)/i', '$1', $part, -1, $replacements); + } + // now remove all fully-qualified elements (those with tables) + $part = preg_replace('/("[a-z][\w]*"|[a-z][\w]*)\.("[a-z][\w]*"|[a-z][\w]*)/i', '', $part); + // finally, match all whole words left behind + preg_match_all('/([a-z][\w]*)/i', $part, $result, PREG_SET_ORDER); + foreach($result as $column) { + $list[] = $column[0]; + } + } + } + return $list; + } + + /** + * Splits the select query up into parts. + * + * @param string $select Original select string + * @return array Individual select components. + */ + private function _splitselect($select) { + // splits a select into parameters, then stores them as + // $select[] = $select_part + $list = array(); + $last_pos = 0; + $pos = -1; + while($pos < strlen($select)) { + $pos++; + if($pos == strlen($select) || $select[$pos] == ',') { + // we found an item, process it + $sel = substr($select, $last_pos, $pos-$last_pos); + if(preg_match('/\sAS\s+"?([a-z]\w*)"?\s*$/i', $sel, $matches) != 0) { + $list[$matches[1]] = trim($sel); + } + $last_pos = $pos+1; + } else if($select[$pos] == '(') { + // skip past parenthesized sections + $pos = $this->_splitselect_parens($select, $pos); + } + } + return $list; + } + + /** + * Recursively processes parentheses in the select string. + * + * @param string $select Select string. + * @param int $pos current location in the string. + * @return int final position after all recursing is complete. + */ + private function _splitselect_parens($select, $pos) { + while($pos < strlen($select)) { + $pos++; + if($select[$pos] == '(') { + // skip past recursive parenthesized sections + $pos = $this->_splitselect_parens($select, $pos); + } else if($select[$pos] == ')') { + break; + } + } + return $pos; + } + +} + +/* End of file rowindex.php */ +/* Location: ./application/datamapper/rowindex.php */ diff --git a/application/datamapper/simplecache.php b/application/datamapper/simplecache.php new file mode 100644 index 0000000..eb5c7f7 --- /dev/null +++ b/application/datamapper/simplecache.php @@ -0,0 +1,70 @@ +_should_delete_cache) ) + { + $object->db->cache_delete(); + $object->_should_delete_cache = FALSE; + } + + $object->db->cache_on(); + // get the arguments, but pop the object. + $args = func_get_args(); + array_shift($args); + call_user_func_array(array($object, 'get'), $args); + $object->db->cache_off(); + return $object; + } + + /** + * Clears the cached query the next time get_cached is called. + * + * @param DataMapper $object The DataMapper Object. + * @return DataMapper The DataMapper $object for chaining. + */ + function clear_cache($object) + { + $args = func_get_args(); + array_shift($args); + if( ! empty($args)) { + call_user_func_array(array($object->db, 'cache_delete'), $args); + } else { + $object->_should_delete_cache = TRUE; + } + return $object; + } + +} + +/* End of file simplecache.php */ +/* Location: ./application/datamapper/simplecache.php */ diff --git a/application/datamapper/translate.php b/application/datamapper/translate.php new file mode 100644 index 0000000..0050ffa --- /dev/null +++ b/application/datamapper/translate.php @@ -0,0 +1,76 @@ +fields; + } + + // loop through the fields + foreach($fields as $f) + { + // first, deal with the loaded fields + if ( isset($object->{$f}) ) + { + $line = lang($object->{$f}); + if ( $line ) + { + $object->{$f}; + } + } + + // then, loop through the all array + foreach($object->all as $key => $all_object) + { + if ( isset($all_object->{$f}) ) + { + $line = lang($all_object->{$f}); + if ( $line ) + { + $object->all[$key]->{$f} = $line; + } + } + } + } + + // return the Datamapper object + return $object; + } + +} + +/* End of file translate.php */ +/* Location: ./application/datamapper/translate.php */ diff --git a/application/helpers/inflector_helper.php b/application/helpers/inflector_helper.php new file mode 100644 index 0000000..25d093e --- /dev/null +++ b/application/helpers/inflector_helper.php @@ -0,0 +1,236 @@ + 4 OR in_array($end3, array('ses', 'hes', 'oes'))) + { + $str = substr($str, 0, -2); + } + elseif (in_array($end2, array('da', 'ia', 'la'))) + { + $str = substr($str, 0, -1).'um'; + } + elseif (in_array($end2, array('bi', 'ei', 'gi', 'li', 'mi', 'pi'))) + { + $str = substr($str, 0, -1).'us'; + } + else + { + if ($end1 == 's' && $end2 != 'us' && $end2 != 'ss') + { + $str = substr($str, 0, -1); + } + } + + return $str; + } +} + +// -------------------------------------------------------------------- + +/** +* Plural +* +* Takes a singular word and makes it plural (improved by stensi) +* +* @access public +* @param string +* @param bool +* @return str +*/ +if ( ! function_exists('plural')) +{ + function plural($str, $force = FALSE) + { + $str = strtolower(trim($str)); + $end3 = substr($str, -3); + $end2 = substr($str, -2); + $end1 = substr($str, -1); + + if ($end3 == 'eau') + { + $str .= 'x'; + } + elseif ($end3 == 'man') + { + $str = substr($str, 0, -2).'en'; + } + elseif (in_array($end3, array('dum', 'ium', 'lum'))) + { + $str = substr($str, 0, -2).'a'; + } + elseif (strlen($str) > 4 && in_array($end3, array('bus', 'eus', 'gus', 'lus', 'mus', 'pus'))) + { + $str = substr($str, 0, -2).'i'; + } + elseif ($end3 == 'ife') + { + $str = substr($str, 0, -2).'ves'; + } + elseif ($end1 == 'f') + { + $str = substr($str, 0, -1).'ves'; + } + elseif ($end1 == 'y') + { + if(preg_match('#[aeiou]y#i', $end2)) + { + // ays, oys, etc. + $str = $str . 's'; + } + else + { + $str = substr($str, 0, -1).'ies'; + } + } + elseif ($end1 == 'o') + { + if(preg_match('#[aeiou]o#i', $end2)) + { + // oos, etc. + $str = $str . 's'; + } + else + { + $str .= 'es'; + } + } + elseif ($end1 == 'x' || in_array($end2, array('ss', 'ch', 'sh')) ) + { + $str .= 'es'; + } + elseif ($end1 == 's') + { + if ($force == TRUE) + { + $str .= 'es'; + } + } + else + { + $str .= 's'; + } + + return $str; + } +} + +// -------------------------------------------------------------------- + +/** + * Camelize + * + * Takes multiple words separated by spaces or underscores and camelizes them + * + * @access public + * @param string + * @return str + */ +if ( ! function_exists('camelize')) +{ + function camelize($str) + { + $str = 'x'.strtolower(trim($str)); + $str = ucwords(preg_replace('/[\s_]+/', ' ', $str)); + return substr(str_replace(' ', '', $str), 1); + } +} + +// -------------------------------------------------------------------- + +/** + * Underscore + * + * Takes multiple words separated by spaces and underscores them + * + * @access public + * @param string + * @return str + */ +if ( ! function_exists('underscore')) +{ + function underscore($str) + { + return preg_replace('/[\s]+/', '_', strtolower(trim($str))); + } +} + +// -------------------------------------------------------------------- + +/** + * Humanize + * + * Takes multiple words separated by underscores and changes them to spaces + * + * @access public + * @param string + * @return str + */ +if ( ! function_exists('humanize')) +{ + function humanize($str) + { + return ucwords(preg_replace('/[_]+/', ' ', strtolower(trim($str)))); + } +} + +/* End of file inflector_helper.php */ +/* Location: ./application/helpers/inflector_helper.php */ \ No newline at end of file diff --git a/application/language/ca/datamapper_lang.php b/application/language/ca/datamapper_lang.php new file mode 100644 index 0000000..d1b0fca --- /dev/null +++ b/application/language/ca/datamapper_lang.php @@ -0,0 +1,25 @@ + 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 */