public function testCustomClassTableMapping() { $this->assertEquals('users', fORM::tablize('User')); $this->assertEquals('User', fORM::classize('users')); fORM::mapClassToTable('User', 'person'); $this->assertEquals('person', fORM::tablize('User')); $this->assertEquals('User', fORM::classize('person')); $this->assertNotEquals('users', fORM::tablize('User')); $this->assertEquals('bicycles', fORM::tablize('Bicycle')); $this->assertEquals('Bicycle', fORM::classize('bicycles')); fORM::mapClassToTable('Bicycle', 'bike'); $this->assertEquals('bike', fORM::tablize('Bicycle')); $this->assertEquals('Bicycle', fORM::classize('bike')); $this->assertNotEquals('bicycles', fORM::tablize('Bicycle')); }
protected function populateRelated($recursive = false) { if ($recursive) { $one_to_many_relationships = $schema->getRelationships($table, 'one-to-many'); foreach ($one_to_many_relationships as $relationship) { $route_name = fORMSchema::getRouteNameFromRelationship('one-to-many', $relationship); $related_class = fORM::classize($relationship['related_table']); $method = 'populate' . fGrammar::pluralize($related_class); $this->{$method}(TRUE, $route_name); } $one_to_one_relationships = $schema->getRelationships($table, 'one-to-one'); foreach ($one_to_one_relationships as $relationship) { $route_name = fORMSchema::getRouteNameFromRelationship('one-to-one', $relationship); $related_class = fORM::classize($relationship['related_table']); $this->__call('populate' . $related_class, array(TRUE, $route_name)); } } return $this; }
/** * 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; }
/** * Validates any many-to-many associations or any one-to-many records that have been flagged for association * * @internal * * @param string $class The class to validate the related records for * @param array &$values The values for the object * @param array &$related_records The related records for the object * @return void */ public static function validate($class, &$values, &$related_records) { $table = fORM::tablize($class); $schema = fORMSchema::retrieve($class); $validation_messages = array(); // Find the record sets to validate foreach ($related_records as $related_table => $routes) { foreach ($routes as $route => $related_info) { if (!$related_info['count'] || !$related_info['associate']) { continue; } $related_class = fORM::classize($related_table); $related_class = fORM::getRelatedClass($class, $related_class); $relationship = fORMSchema::getRoute($schema, $table, $related_table, $route); if (isset($relationship['join_table'])) { $related_messages = self::validateManyToMany($class, $related_class, $route, $related_info); } else { $related_messages = self::validateOneToStar($class, $values, $related_records, $related_class, $route); } $validation_messages = array_merge($validation_messages, $related_messages); } } return $validation_messages; }
/** * Generates a clone of the current record, removing any auto incremented primary key value and allowing for replicating related records * * This method will accept three different sets of parameters: * * - No parameters: this object will be cloned * - A single `TRUE` value: this object plus all many-to-many associations and all child records (recursively) will be cloned * - Any number of plural related record class names: the many-to-many associations or child records that correspond to the classes specified will be cloned * * The class names specified can be a simple class name if there is only a * single route between the two corresponding database tables. If there is * more than one route between the two tables, the class name should be * substituted with a string in the format `'RelatedClass{route}'`. * * @param string $related_class The plural related class to replicate - see method description for details * @param string ... * @return fActiveRecord The cloned record */ public function replicate($related_class = NULL) { fActiveRecord::$replicate_level++; $class = get_class($this); $hash = self::hash($this->values, $class); $table = fORM::tablize($class); // If the object has not been replicated yet, do it now if (!isset(fActiveRecord::$replicate_map[$class])) { fActiveRecord::$replicate_map[$class] = array(); } if (!isset(fActiveRecord::$replicate_map[$class][$hash])) { fActiveRecord::$replicate_map[$class][$hash] = clone $this; // We need the primary key to get a hash, otherwise certain recursive relationships end up losing members $pk_columns = fORMSchema::retrieve()->getKeys($table, 'primary'); if (sizeof($pk_columns) == 1 && fORMSchema::retrieve()->getColumnInfo($table, $pk_columns[0], 'auto_increment')) { fActiveRecord::$replicate_map[$class][$hash]->values[$pk_columns[0]] = $this->values[$pk_columns[0]]; } } $clone = fActiveRecord::$replicate_map[$class][$hash]; $parameters = func_get_args(); $recursive = FALSE; $many_to_many_relationships = fORMSchema::retrieve()->getRelationships($table, 'many-to-many'); $one_to_many_relationships = fORMSchema::retrieve()->getRelationships($table, 'one-to-many'); // When just TRUE is passed we recursively replicate all related records if (sizeof($parameters) == 1 && $parameters[0] === TRUE) { $parameters = array(); $recursive = TRUE; foreach ($many_to_many_relationships as $relationship) { $parameters[] = fGrammar::pluralize(fORM::classize($relationship['related_table'])) . '{' . $relationship['join_table'] . '}'; } foreach ($one_to_many_relationships as $relationship) { $parameters[] = fGrammar::pluralize(fORM::classize($relationship['related_table'])) . '{' . $relationship['related_column'] . '}'; } } $record_sets = array(); foreach ($parameters as $parameter) { // Parse the Class{route} strings if (strpos($parameter, '{') !== FALSE) { $brace = strpos($parameter, '{'); $related_class = fGrammar::singularize(substr($parameter, 0, $brace)); $related_table = fORM::tablize($related_class); $route = substr($parameter, $brace + 1, -1); } else { $related_class = fGrammar::singularize($parameter); $related_table = fORM::tablize($related_class); $route = fORMSchema::getRouteName($table, $related_table); } // Determine the kind of relationship $many_to_many = FALSE; $one_to_many = FALSE; foreach ($many_to_many_relationships as $relationship) { if ($relationship['related_table'] == $related_table && $relationship['join_table'] == $route) { $many_to_many = TRUE; break; } } foreach ($one_to_many_relationships as $relationship) { if ($relationship['related_table'] == $related_table && $relationship['related_column'] == $route) { $one_to_many = TRUE; break; } } if (!$many_to_many && !$one_to_many) { throw new fProgrammerException('The related class specified, %1$s, does not appear to be in a many-to-many or one-to-many relationship with %$2s', $parameter, get_class($this)); } // Get the related records $record_set = fORMRelated::buildRecords($class, $this->values, $this->related_records, $related_class, $route); // One-to-many records need to be replicated, possibly recursively if ($one_to_many) { if ($recursive) { $records = $record_set->call('replicate', TRUE); } else { $records = $record_set->call('replicate'); } $record_set = fRecordSet::buildFromArray($related_class, $records); $record_set->call('set' . fGrammar::camelize($route, TRUE), NULL); } // Cause the related records to be associated with the new clone fORMRelated::associateRecords($class, $clone->related_records, $related_class, $record_set, $route); } fActiveRecord::$replicate_level--; if (!fActiveRecord::$replicate_level) { // This removes the primary keys we had added back in for proper duplicate detection foreach (fActiveRecord::$replicate_map as $class => $records) { $table = fORM::tablize($class); $pk_columns = fORMSchema::retrieve()->getKeys($table, 'primary'); if (sizeof($pk_columns) != 1 || !fORMSchema::retrieve()->getColumnInfo($table, $pk_columns[0], 'auto_increment')) { continue; } foreach ($records as $hash => $record) { $record->values[$pk_columns[0]] = NULL; } } fActiveRecord::$replicate_map = array(); } return $clone; }
/** * Recursivly builds records. * * @param array* $completed_fixtures * Completed records is stored in this array * @param $fixture_data * Build records of this fixture */ private function buildRecords(&$completed_fixtures, $fixture_name, $traverse = TRUE) { if (array_key_exists($fixture_name, $completed_fixtures)) { return; } // Load data if (isset($this->fixture_data[$fixture_name]) === FALSE) { $this->loadFixture($fixture_name); } $class_name = fORM::classize($fixture_name); // If the class does not exists created it if (class_exists($class_name) === FALSE) { fORM::defineActiveRecordClass($class_name); } // Create the records $method_name = NULL; $record = NULL; $records = array(); foreach ($this->fixture_data[$fixture_name] as $record_data) { $record = new $class_name(); foreach ($record_data as $key => $value) { $method_name = 'set' . fGrammar::camelize($key, $upper = TRUE); $value = $this->applyHookCallbacks(self::PreSetBuildHook, $fixture_name, $key, $value); if ($this->isRelationshipKey($fixture_name, $key)) { $related_table = $this->getRelatedTable($fixture_name, $key); $required = $this->isRequiredKey($fixture_name, $key); if ($traverse && array_key_exists($related_table, $completed_fixtures) === FALSE && $fixture_name !== $related_table) { if (isset($value) && array_key_exists($related_table, $this->fixture_sources)) { $this->buildRecords($completed_fixtures, $related_table); array_unshift($this->tables_to_tear_down, $related_table); } } } $record->{$method_name}($value); } $record->store(); $records[] = $record; } $completed_fixtures[$fixture_name] = fRecordSet::buildFromArray($class_name, $records); }
/** * 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; }