/** * Creates the fDatabase::translatedQuery() update statement params * * @return array The parameters for an fDatabase::translatedQuery() SQL update statement */ protected function constructUpdateParams() { $class = get_class($this); $schema = fORMSchema::retrieve($class); $table = fORM::tablize($class); $column_info = $schema->getColumnInfo($table); $assignments = array(); $params = array($table); foreach ($column_info as $column => $info) { if ($info['auto_increment'] && !fActiveRecord::changed($this->values, $this->old_values, $column) && count($column_info) > 1) { continue; } $assignments[] = '%r = ' . $info['placeholder']; $value = fORM::scalarize($class, $column, $this->values[$column]); if ($value === NULL && $info['not_null'] && $info['default'] !== NULL) { $value = $info['default']; } $params[] = $column; $params[] = $value; } $sql = 'UPDATE %r SET ' . join(', ', $assignments) . ' WHERE '; array_unshift($params, $sql); return fORMDatabase::addPrimaryKeyWhereParams($schema, $params, $table, $table, $this->values, $this->old_values); }
/** * 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 * * @return fActiveRecord The record object, to allow for method chaining */ public function store() { $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); try { $table = fORM::tablize($class); $column_info = fORMSchema::retrieve()->getColumnInfo($table); // New auto-incrementing records require lots of special stuff, so we'll detect them here $new_autoincrementing_record = FALSE; if (!$this->exists()) { $pk_columns = fORMSchema::retrieve()->getKeys($table, 'primary'); if (sizeof($pk_columns) == 1 && $column_info[$pk_columns[0]]['auto_increment'] && !$this->values[$pk_columns[0]]) { $new_autoincrementing_record = TRUE; $pk_column = $pk_columns[0]; } } $inside_db_transaction = fORMDatabase::retrieve()->isInsideTransaction(); if (!$inside_db_transaction) { fORMDatabase::retrieve()->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 $sql_values = array(); foreach ($column_info as $column => $info) { $value = fORM::scalarize($class, $column, $this->values[$column]); $sql_values[$column] = fORMDatabase::escapeBySchema($table, $column, $value); } // Most databases don't like the auto incrementing primary key to be set to NULL if ($new_autoincrementing_record && $sql_values[$pk_column] == 'NULL') { unset($sql_values[$pk_column]); } if (!$this->exists()) { $sql = $this->constructInsertSQL($sql_values); } else { $sql = $this->constructUpdateSQL($sql_values); } $result = fORMDatabase::retrieve()->translatedQuery($sql); // If there is an auto-incrementing primary key, grab the value from the database if ($new_autoincrementing_record) { $this->set($pk_column, $result->getAutoIncrementedValue()); } // Storing *-to-many relationships fORMRelated::store($class, $this->values, $this->related_records); fORM::callHookCallbacks($this, 'pre-commit::store()', $this->values, $this->old_values, $this->related_records, $this->cache); if (!$inside_db_transaction) { fORMDatabase::retrieve()->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) { fORMDatabase::retrieve()->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; }
/** * Validates values for an fActiveRecord object against the database schema and any additional rules that have been added * * @internal * * @param fActiveRecord $object The instance of the class to validate * @param array $values The values to validate * @param array $old_values The old values for the record * @return array An array of messages */ public static function validate($object, $values, $old_values) { $class = get_class($object); $table = fORM::tablize($class); $schema = fORMSchema::retrieve($class); self::initializeRuleArrays($class); $validation_messages = array(); // Convert objects into values for validation foreach ($values as $column => $value) { $values[$column] = fORM::scalarize($class, $column, $value); } foreach ($old_values as $column => $column_values) { foreach ($column_values as $key => $value) { $old_values[$column][$key] = fORM::scalarize($class, $column, $value); } } $message_array = self::checkPrimaryKeys($schema, $object, $values, $old_values); if ($message_array) { $validation_messages[key($message_array)] = current($message_array); } $column_info = $schema->getColumnInfo($table); foreach ($column_info as $column => $info) { $message = self::checkAgainstSchema($schema, $object, $column, $values, $old_values); if ($message) { $validation_messages[$column] = $message; } } $messages = self::checkUniqueConstraints($schema, $object, $values, $old_values); if ($messages) { $validation_messages = array_merge($validation_messages, $messages); } foreach (self::$valid_values_rules[$class] as $column => $valid_values) { $message = self::checkValidValuesRule($class, $values, $column, $valid_values); if ($message) { $validation_messages[$column] = $message; } } foreach (self::$regex_rules[$class] as $column => $rule) { $message = self::checkRegexRule($class, $values, $column, $rule['regex'], $rule['message']); if ($message) { $validation_messages[$column] = $message; } } foreach (self::$conditional_rules[$class] as $rule) { $messages = self::checkConditionalRule($schema, $class, $values, $rule['main_columns'], $rule['conditional_values'], $rule['conditional_columns']); if ($messages) { $validation_messages = array_merge($validation_messages, $messages); } } foreach (self::$one_or_more_rules[$class] as $rule) { $message = self::checkOneOrMoreRule($schema, $class, $values, $rule['columns']); if ($message) { $validation_messages[join(',', $rule['columns'])] = $message; } } foreach (self::$only_one_rules[$class] as $rule) { $message = self::checkOnlyOneRule($schema, $class, $values, $rule['columns']); if ($message) { $validation_messages[join(',', $rule['columns'])] = $message; } } return $validation_messages; }
/** * Escapes a value for a DB call based on database schema * * @internal * * @param string $table The table to store the value * @param string $column The column to store the value in, may also be shorthand column name like `table.column` or `table=>related_table.column` or concatenated column names like `table.column||table.other_column` * @param mixed $value The value to escape * @param string $comparison_operator Optional: should be `'='`, `'!='`, `'!'`, `'<>'`, `'<'`, `'<='`, `'>'`, `'>='`, `'IN'`, `'NOT IN'` * @return string The SQL-ready representation of the value */ public static function escapeBySchema($table, $column, $value, $comparison_operator = NULL) { // handle concatenated column names if (preg_match('#\\|\\|#', $column)) { if (is_object($value) && is_callable(array($value, '__toString'))) { $value = $value->__toString(); } elseif (is_object($value)) { $value = (string) $value; } $column_info = array('not_null' => FALSE, 'default' => NULL, 'type' => 'varchar'); } else { // Handle shorthand column names like table.column and table=>related_table.column if (preg_match('#(\\w+)(?:\\{\\w+\\})?\\.(\\w+)$#D', $column, $match)) { $table = $match[1]; $column = $match[2]; } $column_info = fORMSchema::retrieve()->getColumnInfo($table, $column); // Some of the tables being escaped for are linking tables that might break with classize() if (is_object($value)) { $class = fORM::classize($table); $value = fORM::scalarize($class, $column, $value); } } if ($comparison_operator !== NULL) { $comparison_operator = strtr($comparison_operator, array('!' => '<>', '!=' => '<>')); } $valid_comparison_operators = array('=', '!=', '!', '<>', '<=', '<', '>=', '>', 'IN', 'NOT IN'); if ($comparison_operator !== NULL && !in_array(strtoupper($comparison_operator), $valid_comparison_operators)) { throw new fProgrammerException('The comparison operator specified, %1$s, is invalid. Must be one of: %2$s.', $comparison_operator, join(', ', $valid_comparison_operators)); } $co = is_null($comparison_operator) ? '' : ' ' . strtoupper($comparison_operator) . ' '; if ($column_info['not_null'] && $value === NULL && $column_info['default'] !== NULL) { $value = $column_info['default']; } if (is_null($value)) { $prepared_value = 'NULL'; } else { $prepared_value = self::retrieve()->escape($column_info['type'], $value); } if ($prepared_value == 'NULL') { if ($co) { if (in_array(trim($co), array('=', 'IN'))) { $co = ' IS '; } elseif (in_array(trim($co), array('<>', 'NOT IN'))) { $co = ' IS NOT '; } } } return $co . $prepared_value; }