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 .= '';
+ }
+ 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 . '' . $type .'>';
+ }
+ 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 */