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; }
/** * Adds information about methods provided by this class to fActiveRecord * * @internal * * @param string $class The class to reflect the related record methods for * @param array &$signatures The associative array of `{method_name} => {signature}` * @param boolean $include_doc_comments If the doc block comments for each method should be included * @return void */ public static function reflect($class, &$signatures, $include_doc_comments) { $table = fORM::tablize($class); $schema = fORMSchema::retrieve($class); $one_to_one_relationships = $schema->getRelationships($table, 'one-to-one'); $one_to_many_relationships = $schema->getRelationships($table, 'one-to-many'); $many_to_one_relationships = $schema->getRelationships($table, 'many-to-one'); $many_to_many_relationships = $schema->getRelationships($table, 'many-to-many'); $to_one_relationships = array_merge($one_to_one_relationships, $many_to_one_relationships); $to_many_relationships = array_merge($one_to_many_relationships, $many_to_many_relationships); $to_one_created = array(); foreach ($to_one_relationships as $relationship) { $related_class = fORM::classize($relationship['related_table']); $related_class = fORM::getRelatedClass($class, $related_class); if (isset($to_one_created[$related_class])) { continue; } $routes = fORMSchema::getRoutes($schema, $table, $relationship['related_table'], '*-to-one'); $route_names = array(); foreach ($routes as $route) { $route_names[] = fORMSchema::getRouteNameFromRelationship('*-to-one', $route); } $signature = ''; if ($include_doc_comments) { $signature .= "/**\n"; $signature .= " * Creates the related " . $related_class . "\n"; $signature .= " * \n"; if (sizeof($route_names) > 1) { $signature .= " * @param string \$route The route to the related class. Must be one of: '" . join("', '", $route_names) . "'.\n"; } $signature .= " * @return " . $related_class . " The related object\n"; $signature .= " */\n"; } $create_method = 'create' . $related_class; $signature .= 'public function ' . $create_method . '('; if (sizeof($route_names) > 1) { $signature .= '$route'; } $signature .= ')'; $signatures[$create_method] = $signature; $to_one_created[$related_class] = TRUE; } $one_to_one_created = array(); foreach ($one_to_one_relationships as $relationship) { $related_class = fORM::classize($relationship['related_table']); $related_class = fORM::getRelatedClass($class, $related_class); if (isset($one_to_one_created[$related_class])) { continue; } $routes = fORMSchema::getRoutes($schema, $table, $relationship['related_table'], 'one-to-one'); $route_names = array(); foreach ($routes as $route) { $route_names[] = fORMSchema::getRouteNameFromRelationship('one-to-one', $route); } $signature = ''; if ($include_doc_comments) { $signature .= "/**\n"; $signature .= " * Populates the related " . $related_class . "\n"; $signature .= " * \n"; if (sizeof($route_names) > 1) { $signature .= " * @param string \$route The route to the related class. Must be one of: '" . join("', '", $route_names) . "'.\n"; } $signature .= " * @return fActiveRecord The record object, to allow for method chaining\n"; $signature .= " */\n"; } $populate_method = 'populate' . $related_class; $signature .= 'public function ' . $populate_method . '('; if (sizeof($route_names) > 1) { $signature .= '$route'; } $signature .= ')'; $signatures[$populate_method] = $signature; $signature = ''; if ($include_doc_comments) { $signature .= "/**\n"; $signature .= " * Associates the related " . $related_class . " to this record\n"; $signature .= " * \n"; $signature .= " * @param fActiveRecord|array|string|integer \$record The record, or the primary key of the record, to associate\n"; if (sizeof($route_names) > 1) { $signature .= " * @param string \$route The route to the related class. Must be one of: '" . join("', '", $route_names) . "'.\n"; } $signature .= " * @return fActiveRecord The record object, to allow for method chaining\n"; $signature .= " */\n"; } $associate_method = 'associate' . $related_class; $signature .= 'public function ' . $associate_method . '($record'; if (sizeof($route_names) > 1) { $signature .= ', $route'; } $signature .= ')'; $signatures[$associate_method] = $signature; $signature = ''; if ($include_doc_comments) { $signature .= "/**\n"; $signature .= " * Indicates if a related " . $related_class . " exists\n"; $signature .= " * \n"; if (sizeof($route_names) > 1) { $signature .= " * @param string \$route The route to the related class. Must be one of: '" . join("', '", $route_names) . "'.\n"; } $signature .= " * @return boolean If a related record exists\n"; $signature .= " */\n"; } $has_method = 'has' . $related_class; $signature .= 'public function ' . $has_method . '($record'; if (sizeof($route_names) > 1) { $signature .= ', $route'; } $signature .= ')'; $signatures[$has_method] = $signature; $one_to_one_created[$related_class] = TRUE; } $to_many_created = array(); foreach ($to_many_relationships as $relationship) { $related_class = fORM::classize($relationship['related_table']); $related_class = fORM::getRelatedClass($class, $related_class); if (isset($to_many_created[$related_class])) { continue; } $routes = fORMSchema::getRoutes($schema, $table, $relationship['related_table'], '*-to-many'); $route_names = array(); $many_to_many_route_names = array(); $one_to_many_route_names = array(); foreach ($routes as $route) { if (isset($route['join_table'])) { $route_name = fORMSchema::getRouteNameFromRelationship('many-to-many', $route); $route_names[] = $route_name; $many_to_many_route_names[] = $route_name; } else { $route_name = fORMSchema::getRouteNameFromRelationship('one-to-many', $route); $route_names[] = $route_name; $one_to_many_route_names[] = $route_name; } } if ($one_to_many_route_names) { $signature = ''; if ($include_doc_comments) { $related_table = fORM::tablize($related_class); $signature .= "/**\n"; $signature .= " * Calls the ::populate() method for multiple child " . $related_class . " records. Uses request value arrays in the form " . $related_table . "::{column_name}[].\n"; $signature .= " * \n"; if (sizeof($one_to_many_route_names) > 1) { $signature .= " * @param string \$route The route to the related class. Must be one of: '" . join("', '", $one_to_many_route_names) . "'.\n"; } $signature .= " * @return fActiveRecord The record object, to allow for method chaining\n"; $signature .= " */\n"; } $populate_related_method = 'populate' . fGrammar::pluralize($related_class); $signature .= 'public function ' . $populate_related_method . '('; if (sizeof($one_to_many_route_names) > 1) { $signature .= '$route'; } $signature .= ')'; $signatures[$populate_related_method] = $signature; } if ($many_to_many_route_names) { $signature = ''; if ($include_doc_comments) { $related_table = fORM::tablize($related_class); $signature .= "/**\n"; $signature .= " * Creates entries in the appropriate joining table to create associations with the specified " . $related_class . " records. Uses request value array(s) in the form " . $related_table . "::{primary_key_column_name(s)}[].\n"; $signature .= " * \n"; if (sizeof($many_to_many_route_names) > 1) { $signature .= " * @param string \$route The route to the related class. Must be one of: '" . join("', '", $many_to_many_route_names) . "'.\n"; } $signature .= " * @return fActiveRecord The record object, to allow for method chaining\n"; $signature .= " */\n"; } $link_related_method = 'link' . fGrammar::pluralize($related_class); $signature .= 'public function ' . $link_related_method . '('; if (sizeof($many_to_many_route_names) > 1) { $signature .= '$route'; } $signature .= ')'; $signatures[$link_related_method] = $signature; $signature = ''; if ($include_doc_comments) { $related_table = fORM::tablize($related_class); $signature .= "/**\n"; $signature .= " * Creates entries in the appropriate joining table to create associations with the specified " . $related_class . " records\n"; $signature .= " * \n"; $signature .= " * @param fRecordSet|array \$records_to_associate The records to associate - should be an fRecords, an array of records or an array of primary keys\n"; if (sizeof($many_to_many_route_names) > 1) { $signature .= " * @param string \$route The route to the related class. Must be one of: '" . join("', '", $many_to_many_route_names) . "'.\n"; } $signature .= " * @return fActiveRecord The record object, to allow for method chaining\n"; $signature .= " */\n"; } $associate_related_method = 'associate' . fGrammar::pluralize($related_class); $signature .= 'public function ' . $associate_related_method . '($records_to_associate'; if (sizeof($many_to_many_route_names) > 1) { $signature .= ', $route'; } $signature .= ')'; $signatures[$associate_related_method] = $signature; } $signature = ''; if ($include_doc_comments) { $signature .= "/**\n"; $signature .= " * Builds an fRecordSet of the related " . $related_class . " objects\n"; $signature .= " * \n"; if (sizeof($route_names) > 1) { $signature .= " * @param string \$route The route to the related class. Must be one of: '" . join("', '", $route_names) . "'.\n"; } $signature .= " * @return fRecordSet A record set of the related " . $related_class . " objects\n"; $signature .= " */\n"; } $build_method = 'build' . fGrammar::pluralize($related_class); $signature .= 'public function ' . $build_method . '('; if (sizeof($route_names) > 1) { $signature .= '$route'; } $signature .= ')'; $signatures[$build_method] = $signature; $signature = ''; if ($include_doc_comments) { $signature .= "/**\n"; $signature .= " * Indicates if related " . $related_class . " objects exist\n"; $signature .= " * \n"; if (sizeof($route_names) > 1) { $signature .= " * @param string \$route The route to the related class. Must be one of: '" . join("', '", $route_names) . "'.\n"; } $signature .= " * @return boolean If related " . $related_class . " objects exist\n"; $signature .= " */\n"; } $has_method = 'has' . fGrammar::pluralize($related_class); $signature .= 'public function ' . $has_method . '('; if (sizeof($route_names) > 1) { $signature .= '$route'; } $signature .= ')'; $signatures[$has_method] = $signature; $signature = ''; if ($include_doc_comments) { $signature .= "/**\n"; $signature .= " * Returns an array of the primary keys for the related " . $related_class . " objects\n"; $signature .= " * \n"; if (sizeof($route_names) > 1) { $signature .= " * @param string \$route The route to the related class. Must be one of: '" . join("', '", $route_names) . "'.\n"; } $signature .= " * @return array The primary keys of the related " . $related_class . " objects\n"; $signature .= " */\n"; } $list_method = 'list' . fGrammar::pluralize($related_class); $signature .= 'public function ' . $list_method . '('; if (sizeof($route_names) > 1) { $signature .= '$route'; } $signature .= ')'; $signatures[$list_method] = $signature; $signature = ''; if ($include_doc_comments) { $signature .= "/**\n"; $signature .= " * Counts the number of related " . $related_class . " objects\n"; $signature .= " * \n"; if (sizeof($route_names) > 1) { $signature .= " * @param string \$route The route to the related class. Must be one of: '" . join("', '", $route_names) . "'.\n"; } $signature .= " * @return integer The number related " . $related_class . " objects\n"; $signature .= " */\n"; } $count_method = 'count' . fGrammar::pluralize($related_class); $signature .= 'public function ' . $count_method . '('; if (sizeof($route_names) > 1) { $signature .= '$route'; } $signature .= ')'; $signatures[$count_method] = $signature; $to_many_created[$related_class] = TRUE; } }
/** * Deletes a record from the database, but does not destroy the object * * This method will start a database transaction if one is not already active. * * @return fActiveRecord The record object, to allow for method chaining */ public function delete() { $class = get_class($this); if (fORM::getActiveRecordMethod($class, 'delete')) { return $this->__call('delete', array()); } if (!$this->exists()) { throw new fProgrammerException('This %s object does not yet exist in the database, and thus can not be deleted', fORM::getRecordName($class)); } fORM::callHookCallbacks($this, 'pre::delete()', $this->values, $this->old_values, $this->related_records, $this->cache); $table = fORM::tablize($class); $inside_db_transaction = fORMDatabase::retrieve()->isInsideTransaction(); try { if (!$inside_db_transaction) { fORMDatabase::retrieve()->translatedQuery('BEGIN'); } fORM::callHookCallbacks($this, 'post-begin::delete()', $this->values, $this->old_values, $this->related_records, $this->cache); // Check to ensure no foreign dependencies prevent deletion $one_to_many_relationships = fORMSchema::retrieve()->getRelationships($table, 'one-to-many'); $many_to_many_relationships = fORMSchema::retrieve()->getRelationships($table, 'many-to-many'); $relationships = array_merge($one_to_many_relationships, $many_to_many_relationships); $records_sets_to_delete = array(); $restriction_messages = array(); foreach ($relationships as $relationship) { // Figure out how to check for related records $type = isset($relationship['join_table']) ? 'many-to-many' : 'one-to-many'; $route = fORMSchema::getRouteNameFromRelationship($type, $relationship); $related_class = fORM::classize($relationship['related_table']); $related_objects = fGrammar::pluralize($related_class); $method = 'build' . $related_objects; // Grab the related records $record_set = $this->{$method}($route); // If there are none, we can just move on if (!$record_set->count()) { continue; } if ($type == 'one-to-many' && $relationship['on_delete'] == 'cascade') { $records_sets_to_delete[] = $record_set; } if ($relationship['on_delete'] == 'restrict' || $relationship['on_delete'] == 'no_action') { // Otherwise we have a restriction $related_class_name = fORM::classize($relationship['related_table']); $related_record_name = fORM::getRecordName($related_class_name); $related_record_name = fGrammar::pluralize($related_record_name); $restriction_messages[] = self::compose("One or more %s references it", $related_record_name); } } if ($restriction_messages) { throw new fValidationException(self::compose('This %s can not be deleted because:', fORM::getRecordName($class)), $restriction_messages); } // Delete this record $sql = 'DELETE FROM ' . $table . ' WHERE ' . fORMDatabase::createPrimaryKeyWhereClause($table, $table, $this->values, $this->old_values); $result = fORMDatabase::retrieve()->translatedQuery($sql); // Delete related records foreach ($records_sets_to_delete as $record_set) { foreach ($record_set as $record) { if ($record->exists()) { $record->delete(); } } } fORM::callHookCallbacks($this, 'pre-commit::delete()', $this->values, $this->old_values, $this->related_records, $this->cache); if (!$inside_db_transaction) { fORMDatabase::retrieve()->translatedQuery('COMMIT'); } fORM::callHookCallbacks($this, 'post-commit::delete()', $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::delete()', $this->values, $this->old_values, $this->related_records, $this->cache); // Check to see if the validation exception came from a related record, and fix the message if ($e instanceof fValidationException) { $message = $e->getMessage(); $search = self::compose('This %s can not be deleted because:', fORM::getRecordName($class)); if (stripos($message, $search) === FALSE) { $regex = self::compose('This %s can not be deleted because:', '__'); $regex_parts = explode('__', $regex); $regex = '#(' . preg_quote($regex_parts[0], '#') . ').*?(' . preg_quote($regex_parts[0], '#') . ')#'; $message = preg_replace($regex, '\\1' . strtr(fORM::getRecordName($class), array('\\' => '\\\\', '$' => '\\$')) . '\\2', $message); $find = self::compose("One or more %s references it", '__'); $find_parts = explode('__', $find); $find_regex = '#' . preg_quote($find_parts[0], '#') . '(.*?)' . preg_quote($find_parts[1], '#') . '#'; $replace = self::compose("One or more %s indirectly references it", '__'); $replace_parts = explode('__', $replace); $replace_regex = strtr($replace_parts[0], array('\\' => '\\\\', '$' => '\\$')) . '\\1' . strtr($replace_parts[1], array('\\' => '\\\\', '$' => '\\$')); $message = preg_replace($find_regex, $replace_regex, $regex); throw new fValidationException($message); } } throw $e; } fORM::callHookCallbacks($this, 'post::delete()', $this->values, $this->old_values, $this->related_records, $this->cache); // If we just deleted an object that has an auto-incrementing primary key, // lets delete that value from the object since it is no longer valid $pk_columns = fORMSchema::retrieve()->getKeys($table, 'primary'); if (sizeof($pk_columns) == 1 && fORMSchema::retrieve()->getColumnInfo($table, $pk_columns[0], 'auto_increment')) { $this->values[$pk_columns[0]] = NULL; unset($this->old_values[$pk_columns[0]]); } return $this; }