/** * Creates a `WHERE` clause to ensure a database call is only selecting from rows that are part of the same set when an ordering field is in multi-column `UNIQUE` constraint. * * @param string $table The table the `WHERE` clause is for * @param array $other_columns The other columns in the multi-column unique constraint * @param array &$values The values to match with * @return string An SQL `WHERE` clause for the other columns in a multi-column `UNIQUE` constraint */ private static function createOtherFieldsWhereClause($table, $other_columns, &$values) { $conditions = array(); foreach ($other_columns as $other_column) { $conditions[] = $other_column . fORMDatabase::escapeBySchema($table, $other_column, $values[$other_column], '='); } return join(' AND ', $conditions); }
/** * Creates a `WHERE` clause for the primary keys of this record set * * @param string $route The route to this table from another table * @return string The `WHERE` clause */ private function constructWhereClause($route = NULL) { $table = fORM::tablize($this->class); $table_with_route = $route ? $table . '{' . $route . '}' : $table; $pk_columns = fORMSchema::retrieve()->getKeys($table, 'primary'); $sql = ''; // We have a multi-field primary key, making things kinda ugly if (sizeof($pk_columns) > 1) { $conditions = array(); foreach ($this->getPrimaryKeys() as $primary_key) { $sub_conditions = array(); foreach ($pk_columns as $pk_column) { $sub_conditions[] = $table_with_route . '.' . $pk_column . fORMDatabase::escapeBySchema($table, $pk_column, $primary_key[$pk_column], '='); } $conditions[] = join(' AND ', $sub_conditions); } $sql .= '(' . join(') OR (', $conditions) . ')'; // We have a single primary key field, making things nice and easy } else { $first_pk_column = $pk_columns[0]; $values = array(); foreach ($this->getPrimaryKeys() as $primary_key) { $values[] = fORMDatabase::escapeBySchema($table, $first_pk_column, $primary_key); } $sql .= $table_with_route . '.' . $first_pk_column . ' IN (' . join(', ', $values) . ')'; } return $sql; }
/** * 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; }
/** * Associates a set of many-to-many related records with the current record * * @internal * * @param array &$values The current values for the main record being stored * @param array $relationship The information about the relationship between this object and the records in the record set * @param array $related_info An array containing the keys `'record_set'`, `'count'`, `'primary_keys'` and `'associate'` * @return void */ public static function storeManyToMany(&$values, $relationship, $related_info) { $column_value = $values[$relationship['column']]; // First, we remove all existing relationships between the two tables $join_table = $relationship['join_table']; $join_column = $relationship['join_column']; $join_column_value = fORMDatabase::escapeBySchema($join_table, $join_column, $column_value); $delete_sql = 'DELETE FROM ' . $join_table; $delete_sql .= ' WHERE ' . $join_column . ' = ' . $join_column_value; fORMDatabase::retrieve()->translatedQuery($delete_sql); // Then we add back the ones in the record set $join_related_column = $relationship['join_related_column']; $related_pk_columns = fORMSchema::retrieve()->getKeys($relationship['related_table'], 'primary'); $related_column_values = array(); // If the related column is the primary key, we can just use the primary keys if we have them if ($related_pk_columns[0] == $relationship['related_column'] && $related_info['primary_keys']) { $related_column_values = $related_info['primary_keys']; // Otherwise we need to pull the related values out of the record set } else { // If there is no record set, build it from the primary keys if (!$related_info['record_set']) { $related_class = fORM::classize($relationship['related_table']); $related_info['record_set'] = fRecordSet::build($related_class, array($related_pk_columns[0] . '=' => $related_info['primary_keys'])); } $get_related_method_name = 'get' . fGrammar::camelize($relationship['related_column'], TRUE); foreach ($related_info['record_set'] as $record) { $related_column_values[] = $record->{$get_related_method_name}(); } } // Ensure we aren't storing duplicates $related_column_values = array_unique($related_column_values); foreach ($related_column_values as $related_column_value) { $related_column_value = fORMDatabase::escapeBySchema($join_table, $join_related_column, $related_column_value); $insert_sql = 'INSERT INTO ' . $join_table . ' (' . $join_column . ', ' . $join_related_column . ') '; $insert_sql .= 'VALUES (' . $join_column_value . ', ' . $related_column_value . ')'; fORMDatabase::retrieve()->translatedQuery($insert_sql); } }
/** * Creates a `WHERE` clause condition for primary keys of the table specified * * This method requires the `$primary_keys` parameter to be one of: * * - A scalar value for a single-column primary key * - An array of values for a single-column primary key * - An associative array of values for a multi-column primary key (`column => value`) * - An array of associative arrays of values for a multi-column primary key (`key => array(column => value)`) * * If you are looking to build a primary key where clause from the `$values` * and `$old_values` arrays, please see ::createPrimaryKeyWhereClause() * * @internal * * @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 string The `WHERE` clause that will specify the fActiveRecord as it currently exists in the database */ public static function createPrimaryKeyWhereClause($table, $table_alias, &$values, &$old_values) { $primary_keys = fORMSchema::retrieve()->getKeys($table, 'primary'); $sql = ''; foreach ($primary_keys as $primary_key) { if ($sql) { $sql .= " AND "; } $value = isset($old_values[$primary_key]) ? $old_values[$primary_key][0] : $values[$primary_key]; $sql .= $table . '.' . $primary_key . fORMDatabase::escapeBySchema($table, $primary_key, $value, '='); } return $sql; }
/** * 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) { } } }