object_name = strtolower(substr(get_class($this), 0, -6)); $this->object_plural = inflector::plural($this->object_name); if (!isset($this->sorting)) { // Default sorting $this->sorting = array($this->primary_key => 'asc'); } // Initialize database $this->__initialize(); // Clear the object $this->clear(); if (is_object($id)) { // Load an object $this->load_values((array) $id); } elseif (!empty($id)) { // Find an object $this->find($id); } } /** * Prepares the model database connection, determines the table name, * and loads column information. * * @return void */ public function __initialize() { if ( ! is_object($this->db)) { // Get database instance $this->db = Database::instance($this->db); } if (empty($this->table_name)) { // Table name is the same as the object name $this->table_name = $this->object_name; if ($this->table_names_plural === TRUE) { // Make the table name plural $this->table_name = inflector::plural($this->table_name); } } if (is_array($this->ignored_columns)) { // Make the ignored columns mirrored = mirrored $this->ignored_columns = array_combine($this->ignored_columns, $this->ignored_columns); } // Load column information $this->reload_columns(); } /** * Allows serialization of only the object data and state, to prevent * "stale" objects being unserialized, which also requires less memory. * * @return array */ public function __sleep() { // Store only information about the object return array('object_name', 'object', 'changed', 'loaded', 'saved', 'sorting'); } /** * Prepares the database connection and reloads the object. * * @return void */ public function __wakeup() { // Initialize database $this->__initialize(); if ($this->reload_on_wakeup === TRUE) { // Reload the object $this->reload(); } } /** * Handles pass-through to database methods. Calls to query methods * (query, get, insert, update) are not allowed. Query builder methods * are chainable. * * @param string method name * @param array method arguments * @return mixed */ public function __call($method, array $args) { if (method_exists($this->db, $method)) { if (in_array($method, array('query', 'get', 'insert', 'update', 'delete'))) throw new Kohana_Exception('orm.query_methods_not_allowed'); // Method has been applied to the database $this->db_applied[$method] = $method; // Number of arguments passed $num_args = count($args); if ($method === 'select' AND $num_args > 3) { // Call select() manually to avoid call_user_func_array $this->db->select($args); } else { // We use switch here to manually call the database methods. This is // done for speed: call_user_func_array can take over 300% longer to // make calls. Most database methods are 4 arguments or less, so this // avoids almost any calls to call_user_func_array. switch ($num_args) { case 0: if (in_array($method, array('open_paren', 'close_paren', 'enable_cache', 'disable_cache'))) { // Should return ORM, not Database $this->db->$method(); } else { // Support for things like reset_select, reset_write, list_tables return $this->db->$method(); } break; case 1: $this->db->$method($args[0]); break; case 2: $this->db->$method($args[0], $args[1]); break; case 3: $this->db->$method($args[0], $args[1], $args[2]); break; case 4: $this->db->$method($args[0], $args[1], $args[2], $args[3]); break; default: // Here comes the snail... call_user_func_array(array($this->db, $method), $args); break; } } return $this; } else { throw new Kohana_Exception('core.invalid_method', $method, get_class($this)); } } /** * Handles retrieval of all model values, relationships, and metadata. * * @param string column name * @return mixed */ public function __get($column) { if (array_key_exists($column, $this->object)) { return $this->object[$column]; } elseif (isset($this->related[$column])) { return $this->related[$column]; } elseif ($column === 'primary_key_value') { return $this->object[$this->primary_key]; } elseif ($model = $this->related_object($column)) { // This handles the has_one and belongs_to relationships if (in_array($model->object_name, $this->belongs_to) OR ! array_key_exists($this->foreign_key($column), $model->object)) { // Foreign key lies in this table (this model belongs_to target model) OR an invalid has_one relationship $where = array($model->table_name.'.'.$model->primary_key => $this->object[$this->foreign_key($column)]); } else { // Foreign key lies in the target table (this model has_one target model) $where = array($this->foreign_key($column, $model->table_name) => $this->primary_key_value); } // one<>alias:one relationship return $this->related[$column] = $model->find($where); } elseif (isset($this->has_many[$column])) { // Load the "middle" model $through = ORM::factory(inflector::singular($this->has_many[$column])); // Load the "end" model $model = ORM::factory(inflector::singular($column)); // Join ON target model's primary key set to 'through' model's foreign key // User-defined foreign keys must be defined in the 'through' model $join_table = $through->table_name; $join_col1 = $through->foreign_key($model->object_name, $join_table); $join_col2 = $model->table_name.'.'.$model->primary_key; // one<>alias:many relationship return $this->related[$column] = $model ->join($join_table, $join_col1, $join_col2) ->where($through->foreign_key($this->object_name, $join_table), $this->object[$this->primary_key]) ->find_all(); } elseif (in_array($column, $this->has_many)) { // one<>many relationship $model = ORM::factory(inflector::singular($column)); return $this->related[$column] = $model ->where($this->foreign_key($column, $model->table_name), $this->object[$this->primary_key]) ->find_all(); } elseif (in_array($column, $this->has_and_belongs_to_many)) { // Load the remote model, always singular $model = ORM::factory(inflector::singular($column)); if ($this->has($model, TRUE)) { // many<>many relationship return $this->related[$column] = $model ->in($model->table_name.'.'.$model->primary_key, $this->changed_relations[$column]) ->find_all(); } else { // empty many<>many relationship return $this->related[$column] = $model ->where($model->table_name.'.'.$model->primary_key, NULL) ->find_all(); } } elseif (isset($this->ignored_columns[$column])) { return NULL; } elseif (in_array($column, array ( 'object_name', 'object_plural', // Object 'primary_key', 'primary_val', 'table_name', 'table_columns', // Table 'loaded', 'saved', // Status 'has_one', 'belongs_to', 'has_many', 'has_and_belongs_to_many', 'load_with' // Relationships ))) { // Model meta information return $this->$column; } else { throw new Kohana_Exception('core.invalid_property', $column, get_class($this)); } } /** * Handles setting of all model values, and tracks changes between values. * * @param string column name * @param mixed column value * @return void */ public function __set($column, $value) { if (isset($this->ignored_columns[$column])) { return NULL; } elseif (isset($this->object[$column]) OR array_key_exists($column, $this->object)) { if (isset($this->table_columns[$column])) { // Data has changed $this->changed[$column] = $column; // Object is no longer saved $this->saved = FALSE; } $this->object[$column] = $this->load_type($column, $value); } elseif (in_array($column, $this->has_and_belongs_to_many) AND is_array($value)) { // Load relations $model = ORM::factory(inflector::singular($column)); if ( ! isset($this->object_relations[$column])) { // Load relations $this->has($model); } // Change the relationships $this->changed_relations[$column] = $value; if (isset($this->related[$column])) { // Force a reload of the relationships unset($this->related[$column]); } } else { throw new Kohana_Exception('core.invalid_property', $column, get_class($this)); } } /** * Checks if object data is set. * * @param string column name * @return boolean */ public function __isset($column) { return (isset($this->object[$column]) OR isset($this->related[$column])); } /** * Unsets object data. * * @param string column name * @return void */ public function __unset($column) { unset($this->object[$column], $this->changed[$column], $this->related[$column]); } /** * Displays the primary key of a model when it is converted to a string. * * @return string */ public function __toString() { return (string) $this->object[$this->primary_key]; } /** * Returns the values of this object as an array. * * @return array */ public function as_array() { $object = array(); foreach ($this->object as $key => $val) { // Reconstruct the array (calls __get) $object[$key] = $this->$key; } return $object; } /** * Binds another one-to-one object to this model. One-to-one objects * can be nested using 'object1:object2' syntax * * @param string $target_path * @return void */ public function with($target_path) { if (isset($this->with_applied[$target_path])) { // Don't join anything already joined return $this; } // Split object parts $objects = explode(':', $target_path); $target = $this; foreach ($objects as $object) { // Go down the line of objects to find the given target $parent = $target; $target = $parent->related_object($object); if ( ! $target) { // Can't find related object return $this; } } $target_name = $object; // Pop-off top object to get the parent object (user:photo:tag becomes user:photo - the parent table prefix) array_pop($objects); $parent_path = implode(':', $objects); if (empty($parent_path)) { // Use this table name itself for the parent object $parent_path = $this->table_name; } else { if( ! isset($this->with_applied[$parent_path])) { // If the parent object hasn't been joined yet, do it first (otherwise LEFT JOINs fail) $this->with($parent_path); } } // Add to with_applied to prevent duplicate joins $this->with_applied[$target_path] = TRUE; // Use the keys of the empty object to determine the columns $select = array_keys($target->object); foreach ($select as $i => $column) { // Add the prefix so that load_result can determine the relationship $select[$i] = $target_path.'.'.$column.' AS '.$target_path.':'.$column; } // Select all of the prefixed keys in the object $this->db->select($select); if (in_array($target->object_name, $parent->belongs_to) OR ! isset($target->object[$parent->foreign_key($target_name)])) { // Parent belongs_to target, use target's primary key as join column $join_col1 = $target->foreign_key(TRUE, $target_path); $join_col2 = $parent->foreign_key($target_name, $parent_path); } else { // Parent has_one target, use parent's primary key as join column $join_col2 = $parent->foreign_key(TRUE, $parent_path); $join_col1 = $parent->foreign_key($target_name, $target_path); } // This allows for models to use different table prefixes (sharing the same database) $join_table = new Database_Expression($target->db->table_prefix().$target->table_name.' AS '.$this->db->table_prefix().$target_path); // Join the related object into the result $this->db->join($join_table, $join_col1, $join_col2, 'LEFT'); return $this; } /** * Finds and loads a single database row into the object. * * @chainable * @param mixed primary key or an array of clauses * @return ORM */ public function find($id = NULL) { if ($id !== NULL) { if (is_array($id)) { // Search for all clauses $this->db->where($id); } else { // Search for a specific column $this->db->where($this->table_name.'.'.$this->unique_key($id), $id); } } return $this->load_result(); } /** * Finds multiple database rows and returns an iterator of the rows found. * * @chainable * @param integer SQL limit * @param integer SQL offset * @return ORM_Iterator */ public function find_all($limit = NULL, $offset = NULL) { if ($limit !== NULL AND ! isset($this->db_applied['limit'])) { // Set limit $this->limit($limit); } if ($offset !== NULL AND ! isset($this->db_applied['offset'])) { // Set offset $this->offset($offset); } return $this->load_result(TRUE); } /** * Creates a key/value array from all of the objects available. Uses find_all * to find the objects. * * @param string key column * @param string value column * @return array */ public function select_list($key = NULL, $val = NULL) { if ($key === NULL) { $key = $this->primary_key; } if ($val === NULL) { $val = $this->primary_val; } // Return a select list from the results return $this->select($key, $val)->find_all()->select_list($key, $val); } /** * Validates the current object. This method should generally be called * via the model, after the $_POST Validation object has been created. * * @param object Validation array * @return boolean */ public function validate(Validation $array, $save = FALSE) { $safe_array = $array->safe_array(); if ( ! $array->submitted()) { foreach ($safe_array as $key => $value) { // Get the value from this object $value = $this->$key; if (is_object($value) AND $value instanceof ORM_Iterator) { // Convert the value to an array of primary keys $value = $value->primary_key_array(); } // Pre-fill data $array[$key] = $value; } } // Validate the array if ($status = $array->validate()) { // Grab only set fields (excludes missing data, unlike safe_array) $fields = $array->as_array(); foreach ($fields as $key => $value) { if (isset($safe_array[$key])) { // Set new data, ignoring any missing fields or fields without rules $this->$key = $value; } } if ($save === TRUE OR is_string($save)) { // Save this object $this->save(); if (is_string($save)) { // Redirect to the saved page url::redirect($save); } } } // Return validation status return $status; } /** * Saves the current object. * * @chainable * @return ORM */ public function save() { if ( ! empty($this->changed)) { $data = array(); foreach ($this->changed as $column) { // Compile changed data $data[$column] = $this->object[$column]; } if ($this->loaded === TRUE) { $query = $this->db ->where($this->primary_key, $this->object[$this->primary_key]) ->update($this->table_name, $data); // Object has been saved $this->saved = TRUE; } else { $query = $this->db ->insert($this->table_name, $data); if ($query->count() > 0) { if (empty($this->object[$this->primary_key])) { // Load the insert id as the primary key $this->object[$this->primary_key] = $query->insert_id(); } // Object is now loaded and saved $this->loaded = $this->saved = TRUE; } } if ($this->saved === TRUE) { // All changes have been saved $this->changed = array(); } } if ($this->saved === TRUE AND ! empty($this->changed_relations)) { foreach ($this->changed_relations as $column => $values) { // All values that were added $added = array_diff($values, $this->object_relations[$column]); // All values that were saved $removed = array_diff($this->object_relations[$column], $values); if (empty($added) AND empty($removed)) { // No need to bother continue; } // Clear related columns unset($this->related[$column]); // Load the model $model = ORM::factory(inflector::singular($column)); if (($join_table = array_search($column, $this->has_and_belongs_to_many)) === FALSE) continue; if (is_int($join_table)) { // No "through" table, load the default JOIN table $join_table = $model->join_table($this->table_name); } // Foreign keys for the join table $object_fk = $this->foreign_key(NULL); $related_fk = $model->foreign_key(NULL); if ( ! empty($added)) { foreach ($added as $id) { // Insert the new relationship $this->db->insert($join_table, array ( $object_fk => $this->object[$this->primary_key], $related_fk => $id, )); } } if ( ! empty($removed)) { $this->db ->where($object_fk, $this->object[$this->primary_key]) ->in($related_fk, $removed) ->delete($join_table); } // Clear all relations for this column unset($this->object_relations[$column], $this->changed_relations[$column]); } } return $this; } /** * Deletes the current object from the database. This does NOT destroy * relationships that have been created with other objects. * * @chainable * @return ORM */ public function delete($id = NULL) { if ($id === NULL AND $this->loaded) { // Use the the primary key value $id = $this->object[$this->primary_key]; } // Delete this object $this->db->where($this->primary_key, $id)->delete($this->table_name); return $this->clear(); } /** * Delete all objects in the associated table. This does NOT destroy * relationships that have been created with other objects. * * @chainable * @param array ids to delete * @return ORM */ public function delete_all($ids = NULL) { if (is_array($ids)) { // Delete only given ids $this->db->in($this->primary_key, $ids); } elseif (is_null($ids)) { // Delete all records $this->db->where('1=1'); } else { // Do nothing - safeguard return $this; } // Delete all objects $this->db->delete($this->table_name); return $this->clear(); } /** * Unloads the current object and clears the status. * * @chainable * @return ORM */ public function clear() { // Create an array with all the columns set to NULL $columns = array_keys($this->table_columns); $values = array_combine($columns, array_fill(0, count($columns), NULL)); // Replace the current object with an empty one $this->load_values($values); return $this; } /** * Reloads the current object from the database. * * @chainable * @return ORM */ public function reload() { return $this->find($this->object[$this->primary_key]); } /** * Reload column definitions. * * @chainable * @param boolean force reloading * @return ORM */ public function reload_columns($force = FALSE) { if ($force === TRUE OR empty($this->table_columns)) { if (isset(ORM::$column_cache[$this->object_name])) { // Use cached column information $this->table_columns = ORM::$column_cache[$this->object_name]; } else { // Load table columns ORM::$column_cache[$this->object_name] = $this->table_columns = $this->list_fields(); } } return $this; } /** * Tests if this object has a relationship to a different model. * * @param object related ORM model * @param boolean check for any relations to given model * @return boolean */ public function has(ORM $model, $any = FALSE) { // Determine plural or singular relation name $related = ($model->table_names_plural === TRUE) ? $model->object_plural : $model->object_name; if (($join_table = array_search($related, $this->has_and_belongs_to_many)) === FALSE) return FALSE; if (is_int($join_table)) { // No "through" table, load the default JOIN table $join_table = $model->join_table($this->table_name); } if ( ! isset($this->object_relations[$related])) { // Load the object relationships $this->changed_relations[$related] = $this->object_relations[$related] = $this->load_relations($join_table, $model); } if ( ! $model->empty_primary_key()) { // Check if a specific object exists return in_array($model->primary_key_value, $this->changed_relations[$related]); } elseif ($any) { // Check if any relations to given model exist return ! empty($this->changed_relations[$related]); } else { return FALSE; } } /** * Adds a new relationship to between this model and another. * * @param object related ORM model * @return boolean */ public function add(ORM $model) { if ($this->has($model)) return TRUE; // Get the faked column name $column = $model->object_plural; // Add the new relation to the update $this->changed_relations[$column][] = $model->primary_key_value; if (isset($this->related[$column])) { // Force a reload of the relationships unset($this->related[$column]); } return TRUE; } /** * Adds a new relationship to between this model and another. * * @param object related ORM model * @return boolean */ public function remove(ORM $model) { if ( ! $this->has($model)) return FALSE; // Get the faked column name $column = $model->object_plural; if (($key = array_search($model->primary_key_value, $this->changed_relations[$column])) === FALSE) return FALSE; // Remove the relationship unset($this->changed_relations[$column][$key]); if (isset($this->related[$column])) { // Force a reload of the relationships unset($this->related[$column]); } return TRUE; } /** * Count the number of records in the table. * * @return integer */ public function count_all() { // Return the total number of records in a table return $this->db->count_records($this->table_name); } /** * Proxy method to Database list_fields. * * @param string table name or NULL to use this table * @return array */ public function list_fields($table = NULL) { if ($table === NULL) { $table = $this->table_name; } // Proxy to database return $this->db->list_fields($table); } /** * Proxy method to Database field_data. * * @param string table name * @return array */ public function field_data($table) { // Proxy to database return $this->db->field_data($table); } /** * Proxy method to Database field_data. * * @chainable * @param string SQL query to clear * @return ORM */ public function clear_cache($sql = NULL) { // Proxy to database $this->db->clear_cache($sql); ORM::$column_cache = array(); return $this; } /** * Returns the unique key for a specific value. This method is expected * to be overloaded in models if the model has other unique columns. * * @param mixed unique value * @return string */ public function unique_key($id) { return $this->primary_key; } /** * Determines the name of a foreign key for a specific table. * * @param string related table name * @param string prefix table name (used for JOINs) * @return string */ public function foreign_key($table = NULL, $prefix_table = NULL) { if ($table === TRUE) { if (is_string($prefix_table)) { // Use prefix table name and this table's PK return $prefix_table.'.'.$this->primary_key; } else { // Return the name of this table's PK return $this->table_name.'.'.$this->primary_key; } } if (is_string($prefix_table)) { // Add a period for prefix_table.column support $prefix_table .= '.'; } if (isset($this->foreign_key[$table])) { // Use the defined foreign key name, no magic here! $foreign_key = $this->foreign_key[$table]; } else { if ( ! is_string($table) OR ! array_key_exists($table.'_'.$this->primary_key, $this->object)) { // Use this table $table = $this->table_name; if (strpos($table, '.') !== FALSE) { // Hack around support for PostgreSQL schemas list ($schema, $table) = explode('.', $table, 2); } if ($this->table_names_plural === TRUE) { // Make the key name singular $table = inflector::singular($table); } } $foreign_key = $table.'_'.$this->primary_key; } return $prefix_table.$foreign_key; } /** * This uses alphabetical comparison to choose the name of the table. * * Example: The joining table of users and roles would be roles_users, * because "r" comes before "u". Joining products and categories would * result in categories_products, because "c" comes before "p". * * Example: zoo > zebra > robber > ocean > angel > aardvark * * @param string table name * @return string */ public function join_table($table) { if ($this->table_name > $table) { $table = $table.'_'.$this->table_name; } else { $table = $this->table_name.'_'.$table; } return $table; } /** * Returns an ORM model for the given object name; * * @param string object name * @return ORM */ protected function related_object($object) { if (isset($this->has_one[$object])) { $object = ORM::factory($this->has_one[$object]); } elseif (isset($this->belongs_to[$object])) { $object = ORM::factory($this->belongs_to[$object]); } elseif (in_array($object, $this->has_one) OR in_array($object, $this->belongs_to)) { $object = ORM::factory($object); } else { return FALSE; } return $object; } /** * Loads an array of values into into the current object. * * @chainable * @param array values to load * @return ORM */ public function load_values(array $values) { if (array_key_exists($this->primary_key, $values)) { // Replace the object and reset the object status $this->object = $this->changed = $this->related = array(); // Set the loaded and saved object status based on the primary key $this->loaded = $this->saved = ($values[$this->primary_key] !== NULL); } // Related objects $related = array(); foreach ($values as $column => $value) { if (strpos($column, ':') === FALSE) { if (isset($this->table_columns[$column])) { // The type of the value can be determined, convert the value $value = $this->load_type($column, $value); } $this->object[$column] = $value; } else { list ($prefix, $column) = explode(':', $column, 2); $related[$prefix][$column] = $value; } } if ( ! empty($related)) { foreach ($related as $object => $values) { // Load the related objects with the values in the result $this->related[$object] = $this->related_object($object)->load_values($values); } } return $this; } /** * Loads a value according to the types defined by the column metadata. * * @param string column name * @param mixed value to load * @return mixed */ protected function load_type($column, $value) { $type = gettype($value); if ($type == 'object' OR $type == 'array' OR ! isset($this->table_columns[$column])) return $value; // Load column data $column = $this->table_columns[$column]; if ($value === NULL AND ! empty($column['null'])) return $value; if ( ! empty($column['binary']) AND ! empty($column['exact']) AND (int) $column['length'] === 1) { // Use boolean for BINARY(1) fields $column['type'] = 'boolean'; } switch ($column['type']) { case 'int': if ($value === '' AND ! empty($column['null'])) { // Forms will only submit strings, so empty integer values must be null $value = NULL; } elseif ((float) $value > PHP_INT_MAX) { // This number cannot be represented by a PHP integer, so we convert it to a string $value = (string) $value; } else { $value = (int) $value; } break; case 'float': $value = (float) $value; break; case 'boolean': $value = (bool) $value; break; case 'string': $value = (string) $value; break; } return $value; } /** * Loads a database result, either as a new object for this model, or as * an iterator for multiple rows. * * @chainable * @param boolean return an iterator or load a single row * @return ORM for single rows * @return ORM_Iterator for multiple rows */ protected function load_result($array = FALSE) { if ($array === FALSE) { // Only fetch 1 record $this->db->limit(1); } if ( ! isset($this->db_applied['select'])) { // Select all columns by default $this->db->select($this->table_name.'.*'); } if ( ! empty($this->load_with)) { foreach ($this->load_with as $alias => $object) { // Join each object into the results if (is_string($alias)) { // Use alias $this->with($alias); } else { // Use object $this->with($object); } } } if ( ! isset($this->db_applied['orderby']) AND ! empty($this->sorting)) { $sorting = array(); foreach ($this->sorting as $column => $direction) { if (strpos($column, '.') === FALSE) { // Keeps sorting working properly when using JOINs on // tables with columns of the same name $column = $this->table_name.'.'.$column; } $sorting[$column] = $direction; } // Apply the user-defined sorting $this->db->orderby($sorting); } // Load the result $result = $this->db->get($this->table_name); if ($array === TRUE) { // Return an iterated result return new ORM_Iterator($this, $result); } if ($result->count() === 1) { // Load object values $this->load_values($result->result(FALSE)->current()); } else { // Clear the object, nothing was found $this->clear(); } return $this; } /** * Return an array of all the primary keys of the related table. * * @param string table name * @param object ORM model to find relations of * @return array */ protected function load_relations($table, ORM $model) { // Save the current query chain (otherwise the next call will clash) $this->db->push(); $query = $this->db ->select($model->foreign_key(NULL).' AS id') ->from($table) ->where($this->foreign_key(NULL, $table), $this->object[$this->primary_key]) ->get() ->result(TRUE); $this->db->pop(); $relations = array(); foreach ($query as $row) { $relations[] = $row->id; } return $relations; } /** * Returns whether or not primary key is empty * * @return bool */ protected function empty_primary_key() { return (empty($this->object[$this->primary_key]) AND $this->object[$this->primary_key] !== '0'); } } // End ORM