/** * Stores a record in the database, whether existing or new * * This method will start database and filesystem transactions if they have * not already been started. * * @throws fValidationException When ::validate() throws an exception * * @param boolean $force_cascade When storing related records, this will force deleting child records even if they have their own children in a relationship with an RESTRICT or NO ACTION for the ON DELETE clause * @return fActiveRecord The record object, to allow for method chaining */ public function store($force_cascade = FALSE) { $class = get_class($this); if (fORM::getActiveRecordMethod($class, 'store')) { return $this->__call('store', array()); } fORM::callHookCallbacks($this, 'pre::store()', $this->values, $this->old_values, $this->related_records, $this->cache); $db = fORMDatabase::retrieve($class, 'write'); $schema = fORMSchema::retrieve($class); try { $table = fORM::tablize($class); // New auto-incrementing records require lots of special stuff, so we'll detect them here $new_autoincrementing_record = FALSE; if (!$this->exists()) { $pk_columns = $schema->getKeys($table, 'primary'); $pk_column = $pk_columns[0]; $pk_auto_incrementing = $schema->getColumnInfo($table, $pk_column, 'auto_increment'); if (sizeof($pk_columns) == 1 && $pk_auto_incrementing && !$this->values[$pk_column]) { $new_autoincrementing_record = TRUE; } } $inside_db_transaction = $db->isInsideTransaction(); if (!$inside_db_transaction) { $db->translatedQuery('BEGIN'); } fORM::callHookCallbacks($this, 'post-begin::store()', $this->values, $this->old_values, $this->related_records, $this->cache); $this->validate(); fORM::callHookCallbacks($this, 'post-validate::store()', $this->values, $this->old_values, $this->related_records, $this->cache); // Storing main table if (!$this->exists()) { $params = $this->constructInsertParams(); } else { $params = $this->constructUpdateParams(); } $result = call_user_func_array($db->translatedQuery, $params); // If there is an auto-incrementing primary key, grab the value from the database if ($new_autoincrementing_record) { $this->set($pk_column, $result->getAutoIncrementedValue()); } // Fix cascade updated columns for in-memory objects to prevent issues when saving $one_to_one_relationships = $schema->getRelationships($table, 'one-to-one'); $one_to_many_relationships = $schema->getRelationships($table, 'one-to-many'); $relationships = array_merge($one_to_one_relationships, $one_to_many_relationships); foreach ($relationships as $relationship) { $type = in_array($relationship, $one_to_one_relationships) ? 'one-to-one' : 'one-to-many'; $route = fORMSchema::getRouteNameFromRelationship($type, $relationship); $related_table = $relationship['related_table']; $related_class = fORM::classize($related_table); $related_class = fORM::getRelatedClass($class, $related_class); if ($relationship['on_update'] != 'cascade') { continue; } $column = $relationship['column']; if (!fActiveRecord::changed($this->values, $this->old_values, $column)) { continue; } if (!isset($this->related_records[$related_table][$route]['record_set'])) { continue; } $record_set = $this->related_records[$related_table][$route]['record_set']; $related_column = $relationship['related_column']; $old_value = fActiveRecord::retrieveOld($this->old_values, $column); $value = $this->values[$column]; if ($old_value === NULL) { continue; } foreach ($record_set as $record) { if (isset($record->old_values[$related_column])) { foreach (array_keys($record->old_values[$related_column]) as $key) { if ($record->old_values[$related_column][$key] === $old_value) { $record->old_values[$related_column][$key] = $value; } } } if ($record->values[$related_column] === $old_value) { $record->values[$related_column] = $value; } } } // Storing *-to-many and one-to-one relationships fORMRelated::store($class, $this->values, $this->related_records, $force_cascade); fORM::callHookCallbacks($this, 'pre-commit::store()', $this->values, $this->old_values, $this->related_records, $this->cache); if (!$inside_db_transaction) { $db->translatedQuery('COMMIT'); } fORM::callHookCallbacks($this, 'post-commit::store()', $this->values, $this->old_values, $this->related_records, $this->cache); } catch (fException $e) { if (!$inside_db_transaction) { $db->translatedQuery('ROLLBACK'); } fORM::callHookCallbacks($this, 'post-rollback::store()', $this->values, $this->old_values, $this->related_records, $this->cache); if ($new_autoincrementing_record && self::hasOld($this->old_values, $pk_column)) { $this->values[$pk_column] = self::retrieveOld($this->old_values, $pk_column); unset($this->old_values[$pk_column]); } throw $e; } fORM::callHookCallbacks($this, 'post::store()', $this->values, $this->old_values, $this->related_records, $this->cache); $was_new = !$this->exists(); // If we got here we succefully stored, so update old values to make exists() work foreach ($this->values as $column => $value) { $this->old_values[$column] = array($value); } // If the object was just inserted into the database, save it to the identity map if ($was_new) { $hash = self::hash($this->values, $class); if (!isset(self::$identity_map[$class])) { self::$identity_map[$class] = array(); } self::$identity_map[$class][$hash] = $this; } return $this; }
/** * Deletes old files for this record that have been replaced by new ones * * @internal * * @param fActiveRecord $object The fActiveRecord instance * @param array &$values The current values * @param array &$old_values The old values * @param array &$related_records Any records related to this record * @param array &$cache The cache array for the record * @return void */ public static function deleteOld($object, &$values, &$old_values, &$related_records, &$cache) { $class = get_class($object); // Remove the old files for the column foreach (self::$file_upload_columns[$class] as $column => $directory) { $current_file = $values[$column]; foreach (fActiveRecord::retrieveOld($old_values, $column, array(), TRUE) as $file) { if ($file instanceof fFile && (!$current_file instanceof fFile || $current_file->getPath() != $file->getPath())) { $file->delete(); } } } }
/** * Validates values against unique constraints * * @param fSchema $schema The schema object for the object * @param fActiveRecord $object The instance of the class to check * @param array &$values The values to check * @param array &$old_values The old values for the record * @return array An aray of error messages for the unique constraints */ private static function checkUniqueConstraints($schema, $object, &$values, &$old_values) { $class = get_class($object); $table = fORM::tablize($class); $db = fORMDatabase::retrieve($class, 'read'); $key_info = $schema->getKeys($table); $pk_columns = $key_info['primary']; $unique_keys = $key_info['unique']; $messages = array(); foreach ($unique_keys as $unique_columns) { settype($unique_columns, 'array'); // NULL values are unique $found_not_null = FALSE; foreach ($unique_columns as $unique_column) { if ($values[$unique_column] !== NULL) { $found_not_null = TRUE; } } if (!$found_not_null) { continue; } $params = array("SELECT %r FROM %r WHERE ", $key_info['primary'], $table); $column_info = $schema->getColumnInfo($table); $conditions = array(); foreach ($unique_columns as $unique_column) { $value = $values[$unique_column]; // This makes sure the query performs the way an insert will if ($value === NULL && $column_info[$unique_column]['not_null'] && $column_info[$unique_column]['default'] !== NULL) { $value = $column_info[$unique_column]['default']; } if (self::isCaseInsensitive($class, $unique_column) && self::stringlike($value)) { $condition = fORMDatabase::makeCondition($schema, $table, $unique_column, '=', $value); $conditions[] = str_replace('%r', 'LOWER(%r)', $condition); $params[] = $table . '.' . $unique_column; $params[] = fUTF8::lower($value); } else { $conditions[] = fORMDatabase::makeCondition($schema, $table, $unique_column, '=', $value); $params[] = $table . '.' . $unique_column; $params[] = $value; } } $params[0] .= join(' AND ', $conditions); if ($object->exists()) { foreach ($pk_columns as $pk_column) { $value = fActiveRecord::retrieveOld($old_values, $pk_column, $values[$pk_column]); $params[0] .= ' AND ' . fORMDatabase::makeCondition($schema, $table, $pk_column, '<>', $value); $params[] = $table . '.' . $pk_column; $params[] = $value; } } try { $result = call_user_func_array($db->translatedQuery, $params); $result->tossIfNoRows(); // If an exception was not throw, we have existing values $column_names = array(); foreach ($unique_columns as $unique_column) { $column_names[] = fORM::getColumnName($class, $unique_column); } if (sizeof($column_names) == 1) { $messages[join('', $unique_columns)] = self::compose('%sThe value specified must be unique, however it already exists', fValidationException::formatField(join('', $column_names))); } else { $messages[join(',', $unique_columns)] = self::compose('%sThe values specified must be a unique combination, however the specified combination already exists', fValidationException::formatField(join(', ', $column_names))); } } catch (fNoRowsException $e) { } } return $messages; }
/** * Makes sure the ordering value is sane, removes error messages about missing values * * @internal * * @param fActiveRecord $object The fActiveRecord instance * @param array &$values The current values * @param array &$old_values The old values * @param array &$related_records Any records related to this record * @param array &$cache The cache array for the record * @param array &$validation_messages An array of ordered validation messages * @return void */ public static function validate($object, &$values, &$old_values, &$related_records, &$cache, &$validation_messages) { $class = get_class($object); $table = fORM::tablize($class); $db = fORMDatabase::retrieve($class, 'read'); $schema = fORMSchema::retrieve($class); foreach (self::$ordering_columns[$class] as $column => $other_columns) { $current_value = $values[$column]; $old_value = fActiveRecord::retrieveOld($old_values, $column); $params = array("SELECT MAX(%r) FROM %r", $column, $table); if ($other_columns) { $params[0] .= " WHERE "; $params = self::addOtherFieldsWhereParams($schema, $params, $table, $other_columns, $values); } $current_max_value = (int) call_user_func_array($db->translatedQuery, $params)->fetchScalar(); $new_max_value = $current_max_value; if ($new_set = self::isInNewSet($column, $other_columns, $values, $old_values)) { $new_max_value = $current_max_value + 1; $new_set_new_value = fActiveRecord::changed($values, $old_values, $column); } $column_name = fORM::getColumnName($class, $column); // Remove any previous validation warnings $filtered_messages = array(); foreach ($validation_messages as $validation_column => $validation_message) { if (!preg_match('#(^|,)' . preg_quote($column, '#') . '(,|$)#D', $validation_column)) { $filtered_messages[$validation_column] = $validation_message; } } $validation_messages = $filtered_messages; // If we have a completely empty value, we don't need to validate since a valid value will be generated if ($current_value === '' || $current_value === NULL) { continue; } if (!is_numeric($current_value) || strlen((int) $current_value) != strlen($current_value)) { $validation_messages[$column] = self::compose('%sPlease enter an integer', fValidationException::formatField($column_name)); } elseif ($current_value < 1) { $validation_messages[$column] = self::compose('%sThe value can not be less than 1', fValidationException::formatField($column_name)); } } }
/** * Makes sure the ordering value is sane, removes error messages about missing values * * @internal * * @param fActiveRecord $object The fActiveRecord instance * @param array &$values The current values * @param array &$old_values The old values * @param array &$related_records Any records related to this record * @param array &$cache The cache array for the record * @param array &$validation_messages An array of ordered validation messages * @return void */ public static function validate($object, &$values, &$old_values, &$related_records, &$cache, &$validation_messages) { $class = get_class($object); $table = fORM::tablize($class); $column = self::$ordering_columns[$class]['column']; $other_columns = self::$ordering_columns[$class]['other_columns']; $current_value = $values[$column]; $old_value = fActiveRecord::retrieveOld($old_values, $column); $sql = "SELECT max(" . $column . ") FROM " . $table; if ($other_columns) { $sql .= " WHERE " . self::createOtherFieldsWhereClause($table, $other_columns, $values); } $current_max_value = (int) fORMDatabase::retrieve()->translatedQuery($sql)->fetchScalar(); $new_max_value = $current_max_value; if ($new_set = self::isInNewSet($column, $other_columns, $values, $old_values)) { $new_max_value = $current_max_value + 1; $new_set_new_value = fActiveRecord::changed($values, $old_values, $column); } $column_name = fORM::getColumnName($class, $column); // Remove any previous validation warnings $filtered_messages = array(); foreach ($validation_messages as $validation_message) { if (!preg_match('#^' . str_replace('___', '(.*?)', preg_quote(fValidationException::formatField('___' . $column_name . '___'), '#')) . '#', $validation_message)) { $filtered_messages[] = $validation_message; } } $validation_messages = $filtered_messages; // If we have a completely empty value, we don't need to validate since a valid value will be generated if ($current_value === '' || $current_value === NULL) { return; } if (!is_numeric($current_value) || strlen((int) $current_value) != strlen($current_value)) { $validation_messages[] = self::compose('%sPlease enter an integer', fValidationException::formatField($column_name)); } elseif ($current_value < 1) { $validation_messages[] = self::compose('%sThe value can not be less than 1', fValidationException::formatField($column_name)); } }
/** * Add the appropriate SQL and params for a `WHERE` clause condition for primary keys of the table specified * * @internal * * @param fSchema $schema The schema for the database the query will be run on * @param array $params The currently constructed params for fDatabase::query() - the first param should be a SQL statement * @param string $table The table to build the where clause for * @param string $table_alias The alias for the table * @param array &$values The values array for the fActiveRecord object * @param array &$old_values The old values array for the fActiveRecord object * @return array The params to pass to fDatabase::query(), including the new primary key where condition */ public static function addPrimaryKeyWhereParams($schema, $params, $table, $table_alias, &$values, &$old_values) { $pk_columns = $schema->getKeys($table, 'primary'); $column_info = $schema->getColumnInfo($table); $conditions = array(); foreach ($pk_columns as $pk_column) { $value = fActiveRecord::retrieveOld($old_values, $pk_column, $values[$pk_column]); // This makes sure the query performs the way an insert will if ($value === NULL && $column_info[$pk_column]['not_null'] && $column_info[$pk_column]['default'] !== NULL) { $value = $column_info[$pk_column]['default']; } $params[] = $table_alias . '.' . $pk_column; $params[] = $value; $conditions[] = self::makeCondition($schema, $table, $pk_column, '=', $value); } $params[0] .= join(' AND ', $conditions); return $params; }
/** * Validates values against unique constraints * * @param fActiveRecord $object The instance of the class to check * @param array &$values The values to check * @param array &$old_values The old values for the record * @return string An error message for the unique constraints */ private static function checkUniqueConstraints($object, &$values, &$old_values) { $class = get_class($object); $table = fORM::tablize($class); $key_info = fORMSchema::retrieve()->getKeys($table); $primary_keys = $key_info['primary']; $unique_keys = $key_info['unique']; foreach ($unique_keys as $unique_columns) { settype($unique_columns, 'array'); // NULL values are unique $found_not_null = FALSE; foreach ($unique_columns as $unique_column) { if ($values[$unique_column] !== NULL) { $found_not_null = TRUE; } } if (!$found_not_null) { continue; } $sql = "SELECT " . join(', ', $key_info['primary']) . " FROM " . $table . " WHERE "; $first = TRUE; foreach ($unique_columns as $unique_column) { if ($first) { $first = FALSE; } else { $sql .= " AND "; } $value = $values[$unique_column]; if (self::isCaseInsensitive($class, $unique_column) && self::stringlike($value)) { $sql .= 'LOWER(' . $unique_column . ')' . fORMDatabase::escapeBySchema($table, $unique_column, fUTF8::lower($value), '='); } else { $sql .= $unique_column . fORMDatabase::escapeBySchema($table, $unique_column, $value, '='); } } if ($object->exists()) { foreach ($primary_keys as $primary_key) { $value = fActiveRecord::retrieveOld($old_values, $primary_key, $values[$primary_key]); $sql .= ' AND ' . $primary_key . fORMDatabase::escapeBySchema($table, $primary_key, $value, '<>'); } } try { $result = fORMDatabase::retrieve()->translatedQuery($sql); $result->tossIfNoRows(); // If an exception was not throw, we have existing values $column_names = array(); foreach ($unique_columns as $unique_column) { $column_names[] = fORM::getColumnName($class, $unique_column); } if (sizeof($column_names) == 1) { return self::compose('%sThe value specified must be unique, however it already exists', fValidationException::formatField(join('', $column_names))); } else { return self::compose('%sThe values specified must be a unique combination, however the specified combination already exists', fValidationException::formatField(join(', ', $column_names))); } } catch (fNoRowsException $e) { } } }