/** * 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; }
/** * Allows for preloading various data related to the record set in single database queries, as opposed to one query per record * * This method will handle methods in the format `verbRelatedRecords()` for * the verbs `build`, `prebuild`, `precount` and `precreate`. * * `build` calls `create{RelatedClass}()` on each record in the set and * returns the result as a new record set. The relationship route can be * passed as an optional parameter. * * `prebuild` builds *-to-many record sets for all records in the record * set. `precount` will count records in *-to-many record sets for every * record in the record set. `precreate` will create a *-to-one record * for every record in the record set. * * @param string $method_name The name of the method called * @param string $parameters The parameters passed * @return void */ public function __call($method_name, $parameters) { if ($callback = fORM::getRecordSetMethod($method_name)) { return call_user_func_array($callback, array($this, $this->class, &$this->records, $method_name, $parameters)); } list($action, $subject) = fORM::parseMethod($method_name); $route = $parameters ? $parameters[0] : NULL; // This check prevents fGrammar exceptions being thrown when an unknown method is called if (in_array($action, array('build', 'prebuild', 'precount', 'precreate'))) { $related_class = fGrammar::singularize($subject); $related_class_sans_namespace = $related_class; if (!is_array($this->class)) { $related_class = fORM::getRelatedClass($this->class, $related_class); } } switch ($action) { case 'build': if ($route) { $this->precreate($related_class, $route); return $this->buildFromCall('create' . $related_class_sans_namespace, $route); } $this->precreate($related_class); return $this->buildFromCall('create' . $related_class_sans_namespace); case 'prebuild': return $this->prebuild($related_class, $route); case 'precount': return $this->precount($related_class, $route); case 'precreate': return $this->precreate($related_class, $route); } throw new fProgrammerException('Unknown method, %s(), called', $method_name); }
/** * Add a one-to-many rule that requires at least one related record is associated with the current record * * @param mixed $class The class name or instance of the class to add the rule for * @param string $related_class The name of the related class * @param string $route The route to the related class * @return void */ public static function addOneToManyRule($class, $related_class, $route = NULL) { $class = fORM::getClass($class); $related_class = fORM::getRelatedClass($class, $related_class); if (!isset(self::$related_one_or_more_rules[$class])) { self::$related_one_or_more_rules[$class] = array(); } if (!isset(self::$related_one_or_more_rules[$class][$related_class])) { self::$related_one_or_more_rules[$class][$related_class] = array(); } $route = fORMSchema::getRouteName(fORMSchema::retrieve($class), fORM::tablize($class), fORM::tablize($related_class), $route, 'one-to-many'); self::$related_one_or_more_rules[$class][$related_class][$route] = TRUE; }
/** * Handles all method calls for columns, related records and hook callbacks * * Dynamically handles `get`, `set`, `prepare`, `encode` and `inspect` * methods for each column in this record. Method names are in the form * `verbColumName()`. * * This method also handles `associate`, `build`, `count`, `has`, and `link` * verbs for records in many-to-many relationships; `build`, `count`, `has` * and `populate` verbs for all related records in one-to-many relationships * and `create`, `has` and `populate` verbs for all related records in * one-to-one relationships, and the `create` verb for all related records * in many-to-one relationships. * * Method callbacks registered through fORM::registerActiveRecordMethod() * will be delegated via this method. * * @param string $method_name The name of the method called * @param array $parameters The parameters passed * @return mixed The value returned by the method called */ public function __call($method_name, $parameters) { $class = get_class($this); if (!isset(self::$callback_cache[$class][$method_name])) { if (!isset(self::$callback_cache[$class])) { self::$callback_cache[$class] = array(); } $callback = fORM::getActiveRecordMethod($class, $method_name); self::$callback_cache[$class][$method_name] = $callback ? $callback : FALSE; } if ($callback = self::$callback_cache[$class][$method_name]) { return call_user_func_array($callback, array($this, &$this->values, &$this->old_values, &$this->related_records, &$this->cache, $method_name, $parameters)); } if (!isset(self::$method_name_cache[$method_name])) { list($action, $subject) = fORM::parseMethod($method_name); if (in_array($action, array('get', 'encode', 'prepare', 'inspect', 'set'))) { $subject = fGrammar::underscorize($subject); } else { if (in_array($action, array('build', 'count', 'inject', 'link', 'list', 'tally'))) { $subject = fGrammar::singularize($subject); } $subject = fORM::getRelatedClass($class, $subject); } self::$method_name_cache[$method_name] = array('action' => $action, 'subject' => $subject); } else { $action = self::$method_name_cache[$method_name]['action']; $subject = self::$method_name_cache[$method_name]['subject']; } switch ($action) { // Value methods case 'get': return $this->get($subject); case 'encode': if (isset($parameters[0])) { return $this->encode($subject, $parameters[0]); } return $this->encode($subject); case 'prepare': if (isset($parameters[0])) { return $this->prepare($subject, $parameters[0]); } return $this->prepare($subject); case 'inspect': if (isset($parameters[0])) { return $this->inspect($subject, $parameters[0]); } return $this->inspect($subject); case 'set': if (sizeof($parameters) < 1) { throw new fProgrammerException('The method, %s(), requires at least one parameter', $method_name); } return $this->set($subject, $parameters[0]); // Related data methods // Related data methods case 'associate': if (sizeof($parameters) < 1) { throw new fProgrammerException('The method, %s(), requires at least one parameter', $method_name); } $records = $parameters[0]; $route = isset($parameters[1]) ? $parameters[1] : NULL; list($subject, $route, $plural) = self::determineSubject($class, $subject, $route); if ($plural) { fORMRelated::associateRecords($class, $this->related_records, $subject, $records, $route); } else { fORMRelated::associateRecord($class, $this->related_records, $subject, $records, $route); } return $this; case 'build': if (isset($parameters[0])) { return fORMRelated::buildRecords($class, $this->values, $this->related_records, $subject, $parameters[0]); } return fORMRelated::buildRecords($class, $this->values, $this->related_records, $subject); case 'count': if (isset($parameters[0])) { return fORMRelated::countRecords($class, $this->values, $this->related_records, $subject, $parameters[0]); } return fORMRelated::countRecords($class, $this->values, $this->related_records, $subject); case 'create': if (isset($parameters[0])) { return fORMRelated::createRecord($class, $this->values, $this->related_records, $subject, $parameters[0]); } return fORMRelated::createRecord($class, $this->values, $this->related_records, $subject); case 'has': $route = isset($parameters[0]) ? $parameters[0] : NULL; list($subject, $route, ) = self::determineSubject($class, $subject, $route); return fORMRelated::hasRecords($class, $this->values, $this->related_records, $subject, $route); case 'inject': if (sizeof($parameters) < 1) { throw new fProgrammerException('The method, %s(), requires at least one parameter', $method_name); } if (isset($parameters[1])) { return fORMRelated::setRecordSet($class, $this->related_records, $subject, $parameters[0], $parameters[1]); } return fORMRelated::setRecordSet($class, $this->related_records, $subject, $parameters[0]); case 'link': if (isset($parameters[0])) { fORMRelated::linkRecords($class, $this->related_records, $subject, $parameters[0]); } else { fORMRelated::linkRecords($class, $this->related_records, $subject); } return $this; case 'list': if (isset($parameters[0])) { return fORMRelated::getPrimaryKeys($class, $this->values, $this->related_records, $subject, $parameters[0]); } return fORMRelated::getPrimaryKeys($class, $this->values, $this->related_records, $subject); case 'populate': $route = isset($parameters[0]) ? $parameters[0] : NULL; list($subject, $route, ) = self::determineSubject($class, $subject, $route); fORMRelated::populateRecords($class, $this->related_records, $subject, $route); return $this; case 'tally': if (sizeof($parameters) < 1) { throw new fProgrammerException('The method, %s(), requires at least one parameter', $method_name); } if (isset($parameters[1])) { return fORMRelated::setCount($class, $this->related_records, $subject, $parameters[0], $parameters[1]); } return fORMRelated::setCount($class, $this->related_records, $subject, $parameters[0]); // Error handler // Error handler default: throw new fProgrammerException('Unknown method, %s(), called', $method_name); } }
/** * Sets the ordering to use when returning an fRecordSet of related objects * * @param mixed $class The class name or instance of the class this ordering rule applies to * @param string $related_class The related class we are getting info from * @param array $order_bys An array of the order bys for this table.column combination - see fRecordSet::build() for format * @param string $route The route to the related table, this should be a column name in the current table or a join table name * @return void */ public static function setOrderBys($class, $related_class, $order_bys, $route = NULL) { fActiveRecord::validateClass($related_class); fActiveRecord::forceConfigure($related_class); $class = fORM::getClass($class); $table = fORM::tablize($class); $related_class = fORM::getRelatedClass($class, $related_class); $related_table = fORM::tablize($related_class); $schema = fORMSchema::retrieve($class); $route = fORMSchema::getRouteName($schema, $table, $related_table, $route, '*-to-many'); if (!isset(self::$order_bys[$table])) { self::$order_bys[$table] = array(); } if (!isset(self::$order_bys[$table][$related_table])) { self::$order_bys[$table][$related_table] = array(); } self::$order_bys[$table][$related_table][$route] = $order_bys; }