/** * Sets a column to be formatted as an fMoney object * * @param mixed $class The class name or instance of the class to set the column format * @param string $column The column to format as an fMoney object * @param string $currency_column If specified, this column will store the currency of the fMoney object * @return void */ public static function configureMoneyColumn($class, $column, $currency_column = NULL) { $class = fORM::getClass($class); $table = fORM::tablize($class); $schema = fORMSchema::retrieve($class); $data_type = $schema->getColumnInfo($table, $column, 'type'); $valid_data_types = array('float'); if (!in_array($data_type, $valid_data_types)) { throw new fProgrammerException('The column specified, %1$s, is a %2$s column. Must be %3$s to be set as a money column.', $column, $data_type, join(', ', $valid_data_types)); } if ($currency_column !== NULL) { $currency_column_data_type = $schema->getColumnInfo($table, $currency_column, 'type'); $valid_currency_column_data_types = array('varchar', 'char', 'text'); if (!in_array($currency_column_data_type, $valid_currency_column_data_types)) { throw new fProgrammerException('The currency column specified, %1$s, is a %2$s column. Must be %3$s to be set as a currency column.', $currency_column, $currency_column_data_type, join(', ', $valid_currency_column_data_types)); } } $camelized_column = fGrammar::camelize($column, TRUE); fORM::registerActiveRecordMethod($class, 'encode' . $camelized_column, self::encodeMoneyColumn); fORM::registerActiveRecordMethod($class, 'prepare' . $camelized_column, self::prepareMoneyColumn); if (!fORM::checkHookCallback($class, 'post::validate()', self::validateMoneyColumns)) { fORM::registerHookCallback($class, 'post::validate()', self::validateMoneyColumns); } fORM::registerReflectCallback($class, self::reflect); fORM::registerInspectCallback($class, $column, self::inspect); $value = FALSE; if ($currency_column) { $value = $currency_column; if (empty(self::$currency_columns[$class])) { self::$currency_columns[$class] = array(); } self::$currency_columns[$class][$currency_column] = $column; if (!fORM::checkHookCallback($class, 'post::loadFromResult()', self::makeMoneyObjects)) { fORM::registerHookCallback($class, 'post::loadFromResult()', self::makeMoneyObjects); } if (!fORM::checkHookCallback($class, 'pre::validate()', self::makeMoneyObjects)) { fORM::registerHookCallback($class, 'pre::validate()', self::makeMoneyObjects); } fORM::registerActiveRecordMethod($class, 'set' . $camelized_column, self::setMoneyColumn); fORM::registerActiveRecordMethod($class, 'set' . fGrammar::camelize($currency_column, TRUE), self::setCurrencyColumn); } else { fORM::registerObjectifyCallback($class, $column, self::objectifyMoney); } if (empty(self::$money_columns[$class])) { self::$money_columns[$class] = array(); } self::$money_columns[$class][$column] = $value; }
/** * 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 one-to-* related records * * @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 * @param string $related_class The name of the class for this record set * @param string $route The route between the table and related table * @return array An array of validation messages */ private static function validateOneToStar($class, &$values, &$related_records, $related_class, $route) { $schema = fORMSchema::retrieve($class); $table = fORM::tablize($class); $related_table = fORM::tablize($related_class); $relationship = fORMSchema::getRoute($schema, $table, $related_table, $route); $first_pk_column = self::determineFirstPKColumn($class, $related_class, $route); $filter = self::determineRequestFilter($class, $related_class, $route); $pk_field = $filter . $first_pk_column; $input_keys = array_keys(fRequest::get($pk_field, 'array', array())); $related_record_name = self::getRelatedRecordName($class, $related_class, $route); $messages = array(); $one_to_one = fORMSchema::isOneToOne($schema, $table, $related_table, $route); if ($one_to_one) { $records = array(self::createRecord($class, $values, $related_records, $related_class, $route)); } else { $records = self::buildRecords($class, $values, $related_records, $related_class, $route); } foreach ($records as $i => $record) { fRequest::filter($filter, isset($input_keys[$i]) ? $input_keys[$i] : $i); $record_messages = $record->validate(TRUE); foreach ($record_messages as $column => $record_message) { // Ignore validation messages about the primary key since it will be added if ($column == $relationship['related_column']) { continue; } if ($one_to_one) { $token_field = fValidationException::formatField('__TOKEN__'); $extract_message_regex = '#' . str_replace('__TOKEN__', '(.*?)', preg_quote($token_field, '#')) . '(.*)$#D'; preg_match($extract_message_regex, $record_message, $matches); $column_name = self::compose('%1$s %2$s', $related_record_name, $matches[1]); $messages[$related_table . '::' . $column] = self::compose('%1$s%2$s', fValidationException::formatField($column_name), $matches[2]); } else { $main_key = $related_table . '[' . $i . ']'; if (!isset($messages[$main_key])) { if (isset(self::$validation_name_methods[$class][$related_class][$route])) { $name = $record->{self::$validation_name_methods[$class][$related_class][$route]}($i + 1); } else { $name = $related_record_name . ' #' . ($i + 1); } $messages[$main_key] = array('name' => $name, 'errors' => array()); } $messages[$main_key]['errors'][$column] = $record_message; } } fRequest::unfilter(); } return $messages; }
/** * Sets a timestamp column to store the timezone in another column * * Since not all databases support timezone information in timestamp * columns, this method allows storing the timezone in another columns. * When the timestamp and timezone are retrieved from the database, they * will be automatically combined together into an fTimestamp object. * * @param mixed $class The class name or instance of the class to set the column format * @param string $timestamp_column The timestamp column to store the timezone for * @param string $timezone_column The column to store the timezone in * @return void */ public static function configureTimezoneColumn($class, $timestamp_column, $timezone_column) { $class = fORM::getClass($class); $table = fORM::tablize($class); $schema = fORMSchema::retrieve($class); $timestamp_data_type = $schema->getColumnInfo($table, $timestamp_column, 'type'); if ($timestamp_data_type != 'timestamp') { throw new fProgrammerException('The timestamp column specified, %1$s, is a %2$s column. Must be a %3$s to have a related timezone column.', $timestamp_column, $data_type, 'timestamp'); } $timezone_column_data_type = $schema->getColumnInfo($table, $timezone_column, 'type'); $valid_timezone_column_data_types = array('varchar', 'char', 'text'); if (!in_array($timezone_column_data_type, $valid_timezone_column_data_types)) { throw new fProgrammerException('The timezone column specified, %1$s, is a %2$s column. Must be %3$s to be set as a timezone column.', $timezone_column, $timezone_column_data_type, join(', ', $valid_timezone_column_data_types)); } if (!fORM::checkHookCallback($class, 'post::validate()', self::validateTimezoneColumns)) { fORM::registerHookCallback($class, 'post::validate()', self::validateTimezoneColumns); } if (!fORM::checkHookCallback($class, 'post::loadFromResult()', self::makeTimestampObjects)) { fORM::registerHookCallback($class, 'post::loadFromResult()', self::makeTimestampObjects); } if (!fORM::checkHookCallback($class, 'pre::validate()', self::makeTimestampObjects)) { fORM::registerHookCallback($class, 'pre::validate()', self::makeTimestampObjects); } fORM::registerInspectCallback($class, $timezone_column, self::inspect); fORM::registerActiveRecordMethod($class, 'set' . fGrammar::camelize($timestamp_column, TRUE), self::setTimestampColumn); fORM::registerActiveRecordMethod($class, 'set' . fGrammar::camelize($timezone_column, TRUE), self::setTimezoneColumn); if (empty(self::$timestamp_columns[$class])) { self::$timestamp_columns[$class] = array(); } self::$timestamp_columns[$class][$timestamp_column] = $timezone_column; if (empty(self::$timezone_columns[$class])) { self::$timezone_columns[$class] = array(); } self::$timezone_columns[$class][$timezone_column] = $timestamp_column; }
/** * Creates the objects for related records that are in a one-to-one or many-to-one relationship with the current class in a single DB query * * @param string $related_class This should be the name of a related class * @param string $route This should be the column name of the foreign key and is only required when there are multiple routes to a related table. If there are multiple routes and this is not specified, an fProgrammerException will be thrown. * @return fRecordSet The record set object, to allow for method chaining */ private function precreate($related_class, $route = NULL) { if (!$this->records) { return $this; } $this->validateSingleClass('precreate'); // If there are no primary keys we can just exit if (!array_merge($this->getPrimaryKeys())) { return $this; } fActiveRecord::validateClass($related_class); fActiveRecord::forceConfigure($related_class); $relationship = fORMSchema::getRoute(fORMSchema::retrieve($this->class), fORM::tablize($this->class), fORM::tablize($related_class), $route, '*-to-one'); $values = $this->call('get' . fGrammar::camelize($relationship['column'], TRUE)); $values = array_unique($values); self::build($related_class, array($relationship['related_column'] . '=' => $values)); 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; }
/** * Makes sure the ordering value is sane, removes error messages about missing values * * @internal * * @param fActiveRecord $object The fActiveRecord instance * @param array &$values The current values * @param array &$old_values The old values * @param array &$related_records Any records related to this record * @param array &$cache The cache array for the record * @param array &$validation_messages An array of ordered validation messages * @return void */ public static function validate($object, &$values, &$old_values, &$related_records, &$cache, &$validation_messages) { $class = get_class($object); $table = fORM::tablize($class); $db = fORMDatabase::retrieve($class, 'read'); $schema = fORMSchema::retrieve($class); foreach (self::$ordering_columns[$class] as $column => $other_columns) { $current_value = $values[$column]; $old_value = fActiveRecord::retrieveOld($old_values, $column); $params = array("SELECT MAX(%r) FROM %r", $column, $table); if ($other_columns) { $params[0] .= " WHERE "; $params = self::addOtherFieldsWhereParams($schema, $params, $table, $other_columns, $values); } $current_max_value = (int) call_user_func_array($db->translatedQuery, $params)->fetchScalar(); $new_max_value = $current_max_value; if ($new_set = self::isInNewSet($column, $other_columns, $values, $old_values)) { $new_max_value = $current_max_value + 1; $new_set_new_value = fActiveRecord::changed($values, $old_values, $column); } $column_name = fORM::getColumnName($class, $column); // Remove any previous validation warnings $filtered_messages = array(); foreach ($validation_messages as $validation_column => $validation_message) { if (!preg_match('#(^|,)' . preg_quote($column, '#') . '(,|$)#D', $validation_column)) { $filtered_messages[$validation_column] = $validation_message; } } $validation_messages = $filtered_messages; // If we have a completely empty value, we don't need to validate since a valid value will be generated if ($current_value === '' || $current_value === NULL) { continue; } if (!is_numeric($current_value) || strlen((int) $current_value) != strlen($current_value)) { $validation_messages[$column] = self::compose('%sPlease enter an integer', fValidationException::formatField($column_name)); } elseif ($current_value < 1) { $validation_messages[$column] = self::compose('%sThe value can not be less than 1', fValidationException::formatField($column_name)); } } }
/** * Finds all of the table names in the SQL and creates the appropriate `FROM` and `GROUP BY` clauses with all necessary joins * * The SQL string should contain two placeholders, `:from_clause` and * `:group_by_clause`. All columns should be qualified with their full table * name. Here is an example SQL string to pass in presumming that the * tables users and groups are in a relationship: * * {{{ * SELECT users.* FROM :from_clause WHERE groups.group_id = 5 :group_by_clause ORDER BY lower(users.first_name) ASC * }}} * * @internal * * @param string $table The main table to be queried * @param string $sql The SQL to insert the `FROM` clause into * @return string The SQL `FROM` clause */ public static function insertFromAndGroupByClauses($table, $sql) { $joins = array(); if (strpos($sql, ':from_clause') === FALSE) { throw new fProgrammerException("No %1\$s placeholder was found in:%2\$s", ':from_clause', "\n" . $sql); } if (strpos($sql, ':group_by_clause') === FALSE && !preg_match('#group\\s+by#i', $sql)) { throw new fProgrammerException("No %1\$s placeholder was found in:%2\$s", ':group_by_clause', "\n" . $sql); } $has_group_by_placeholder = strpos($sql, ':group_by_clause') !== FALSE ? TRUE : FALSE; // Separate the SQL from quoted values preg_match_all("#(?:'(?:''|\\\\'|\\\\[^']|[^'\\\\])*')|(?:[^']+)#", $sql, $matches); $table_alias = $table; $used_aliases = array(); $table_map = array(); // If we are not passing in existing joins, start with the specified table if (!$joins) { $joins[] = array('join_type' => 'none', 'table_name' => $table, 'table_alias' => $table_alias); } $used_aliases[] = $table_alias; foreach ($matches[0] as $match) { if ($match[0] != "'") { preg_match_all('#\\b((?:(\\w+)(?:\\{(\\w+)\\})?=>)?(\\w+)(?:\\{(\\w+)\\})?)\\.\\w+\\b#m', $match, $table_matches, PREG_SET_ORDER); foreach ($table_matches as $table_match) { if (!isset($table_match[5])) { $table_match[5] = NULL; } // This is a related table that is going to join to a once-removed table if (!empty($table_match[2])) { $related_table = $table_match[2]; $route = fORMSchema::getRouteName($table, $related_table, $table_match[3]); $join_name = $table . '_' . $related_table . '{' . $route . '}'; self::createJoin($table, $table_alias, $related_table, $route, $joins, $used_aliases); $once_removed_table = $table_match[4]; $route = fORMSchema::getRouteName($related_table, $once_removed_table, $table_match[5]); $join_name = self::createJoin($related_table, $joins[$join_name]['table_alias'], $once_removed_table, $route, $joins, $used_aliases); $table_map[$table_match[1]] = $joins[$join_name]['table_alias']; // This is a related table } elseif (($table_match[4] != $table || fORMSchema::getRoutes($table, $table_match[4])) && $table_match[1] != $table) { $related_table = $table_match[4]; $route = fORMSchema::getRouteName($table, $related_table, $table_match[5]); // If the related table is the current table and it is a one-to-many we don't want to join if ($table_match[4] == $table) { $one_to_many_routes = fORMSchema::getRoutes($table, $related_table, 'one-to-many'); if (isset($one_to_many_routes[$route])) { $table_map[$table_match[1]] = $table_alias; continue; } } $join_name = self::createJoin($table, $table_alias, $related_table, $route, $joins, $used_aliases); $table_map[$table_match[1]] = $joins[$join_name]['table_alias']; } } } } // Determine if we joined a *-to-many relationship $joined_to_many = FALSE; foreach ($joins as $name => $join) { if (is_numeric($name)) { continue; } if (substr($name, -5) == '_join') { $joined_to_many = TRUE; break; } $main_table = preg_replace('#_' . $join['table_name'] . '{\\w+}$#iD', '', $name); $second_table = $join['table_name']; $route = preg_replace('#^[^{]+{(\\w+)}$#D', '\\1', $name); $routes = fORMSchema::getRoutes($main_table, $second_table, '*-to-many'); if (isset($routes[$route])) { $joined_to_many = TRUE; break; } } $found_order_by = FALSE; $from_clause = self::createFromClauseFromJoins($joins); // If we are joining on a *-to-many relationship we need to group by the // columns in the main table to prevent duplicate entries if ($joined_to_many) { $column_info = fORMSchema::retrieve()->getColumnInfo($table); $group_by_clause = ' GROUP BY '; $columns = array(); foreach ($column_info as $column => $info) { $columns[] = $table . '.' . $column; } $group_by_columns = join(', ', $columns) . ' '; $group_by_clause .= $group_by_columns; } else { $group_by_clause = ' '; $group_by_columns = ''; } // Put the SQL back together $new_sql = ''; foreach ($matches[0] as $match) { $temp_sql = $match; // Get rid of the => notation and the :from_clause placeholder if ($match[0] !== "'") { foreach ($table_map as $arrow_table => $alias) { $temp_sql = str_replace($arrow_table, $alias, $temp_sql); } // In the ORDER BY clause we need to wrap columns in if ($found_order_by && $joined_to_many) { $temp_sql = preg_replace('#(?<!avg\\(|count\\(|max\\(|min\\(|sum\\(|cast\\(|case |when )\\b((?!' . preg_quote($table, '#') . '\\.)\\w+\\.\\w+)\\b#i', 'max(\\1)', $temp_sql); } if ($joined_to_many && preg_match('#order\\s+by#i', $temp_sql)) { $order_by_found = TRUE; $parts = preg_split('#(order\\s+by)#i', $temp_sql, -1, PREG_SPLIT_DELIM_CAPTURE); $parts[2] = $temp_sql = preg_replace('#(?<!avg\\(|count\\(|max\\(|min\\(|sum\\(|cast\\(|case |when )\\b((?!' . preg_quote($table, '#') . '\\.)\\w+\\.\\w+)\\b#i', 'max(\\1)', $parts[2]); $temp_sql = join('', $parts); } $temp_sql = str_replace(':from_clause', $from_clause, $temp_sql); if ($has_group_by_placeholder) { $temp_sql = preg_replace('#\\s:group_by_clause\\s#', strtr($group_by_clause, array('\\' => '\\\\', '$' => '\\$')), $temp_sql); } elseif ($group_by_columns) { $temp_sql = preg_replace('#(\\sGROUP\\s+BY\\s((?!HAVING|ORDER\\s+BY).)*)\\s#i', '\\1, ' . strtr($group_by_columns, array('\\' => '\\\\', '$' => '\\$')), $temp_sql); } } $new_sql .= $temp_sql; } return $new_sql; }
/** * Returns the metadata about a column including features added by this class * * @internal * * @param fActiveRecord $object The fActiveRecord instance * @param array &$values The current values * @param array &$old_values The old values * @param array &$related_records Any records related to this record * @param array &$cache The cache array for the record * @param string $method_name The method that was called * @param array $parameters The parameters passed to the method * @return mixed The metadata array or element specified */ public static function inspect($object, &$values, &$old_values, &$related_records, &$cache, $method_name, $parameters) { list($action, $column) = fORM::parseMethod($method_name); $class = get_class($object); $table = fORM::tablize($class); $info = fORMSchema::retrieve()->getColumnInfo($table, $column); $element = isset($parameters[0]) ? $parameters[0] : NULL; $column = self::$ordering_columns[$class]['column']; $other_columns = self::$ordering_columns[$class]['other_columns']; // Retrieve the current max ordering index from the database $sql = "SELECT max(" . $column . ") FROM " . $table; if ($other_columns) { $sql .= " WHERE " . self::createOtherFieldsWhereClause($table, $other_columns, $values); } $max_value = (int) fORMDatabase::retrieve()->translatedQuery($sql)->fetchScalar(); // If this is a new record, or in a new set, we need one more space in the ordering index if (self::isInNewSet($column, $other_columns, $values, $old_values)) { $max_value += 1; } $info['max_ordering_value'] = $max_value; $info['feature'] = 'ordering'; fORM::callInspectCallbacks($class, $column, $info); if ($element) { return isset($info[$element]) ? $info[$element] : NULL; } return $info; }
/** * Generates phpdoc for class * @return string */ public function reflectPhpDoc() { $signatures = array(); $class = get_class($this); $table = fORM::tablize($class); $schema = fORMSchema::retrieve($class); foreach ($schema->getColumnInfo($table) as $column => $columnInfo) { $camelizedColumn = fGrammar::camelize($column, TRUE); // Get and set methods $fixedType = $columnInfo['type']; if ($fixedType == 'blob') { $fixedType = 'string'; } if ($fixedType == 'varchar') { $fixedType = 'string'; } if ($fixedType == 'date') { $fixedType = 'fDate|string'; } if ($fixedType == 'timestamp') { $fixedType = 'fTimestamp|string'; } if ($fixedType == 'time') { $fixedType = 'fTime|string'; } $firstFixedType = reset(explode('|', $fixedType)); $signatures[] = $this->generateMagicMethodPhpDoc('get' . $camelizedColumn, array(), $firstFixedType, "Gets the current value of {$column}"); $signatures[] = $this->generateMagicMethodPhpDoc('set' . $camelizedColumn, array($fixedType => $column), $class, "Sets the value for {$column}"); } return $signatures; }
/** * Adjusts the fActiveRecord::reflect() signatures of columns that have been configured in this class * * @internal * * @param string $class The class to reflect * @param array &$signatures The associative array of `{method name} => {signature}` * @param boolean $include_doc_comments If doc comments should be included with the signature * @return void */ public static function reflect($class, &$signatures, $include_doc_comments) { if (isset(self::$link_columns[$class])) { foreach (self::$link_columns[$class] as $column => $enabled) { $signature = ''; if ($include_doc_comments) { $signature .= "/**\n"; $signature .= " * Prepares the value of " . $column . " for output into HTML\n"; $signature .= " * \n"; $signature .= " * This method will ensure all links that start with a domain name are preceeded by http://\n"; $signature .= " * \n"; $signature .= " * @param boolean \$create_link Will cause link to be automatically converted into an [a] tag\n"; $signature .= " * @return string The HTML-ready value\n"; $signature .= " */\n"; } $prepare_method = 'prepare' . fGrammar::camelize($column, TRUE); $signature .= 'public function ' . $prepare_method . '($create_link=FALSE)'; $signatures[$prepare_method] = $signature; } } if (isset(self::$number_columns[$class])) { $table = fORM::tablize($class); $schema = fORMSchema::retrieve($class); foreach (self::$number_columns[$class] as $column => $enabled) { $camelized_column = fGrammar::camelize($column, TRUE); $type = $schema->getColumnInfo($table, $column, 'type'); // Get and set methods $signature = ''; if ($include_doc_comments) { $signature .= "/**\n"; $signature .= " * Gets the current value of " . $column . "\n"; $signature .= " * \n"; $signature .= " * @return fNumber The current value\n"; $signature .= " */\n"; } $get_method = 'get' . $camelized_column; $signature .= 'public function ' . $get_method . '()'; $signatures[$get_method] = $signature; $signature = ''; if ($include_doc_comments) { $signature .= "/**\n"; $signature .= " * Sets the value for " . $column . "\n"; $signature .= " * \n"; $signature .= " * @param fNumber|string|integer \$" . $column . " The new value - don't use floats since they are imprecise\n"; $signature .= " * @return fActiveRecord The record object, to allow for method chaining\n"; $signature .= " */\n"; } $set_method = 'set' . $camelized_column; $signature .= 'public function ' . $set_method . '($' . $column . ')'; $signatures[$set_method] = $signature; $signature = ''; if ($include_doc_comments) { $signature .= "/**\n"; $signature .= " * Encodes the value of " . $column . " for output into an HTML form\n"; $signature .= " * \n"; $signature .= " * If the value is an fNumber object, the ->__toString() method will be called\n"; $signature .= " * resulting in the value without any thousands separators\n"; $signature .= " * \n"; if ($type == 'float') { $signature .= " * @param integer \$decimal_places The number of decimal places to display - not passing any value or passing NULL will result in the intrisinc number of decimal places being shown\n"; } $signature .= " * @return string The HTML form-ready value\n"; $signature .= " */\n"; } $encode_method = 'encode' . $camelized_column; $signature .= 'public function ' . $encode_method . '('; if ($type == 'float') { $signature .= '$decimal_places=NULL'; } $signature .= ')'; $signatures[$encode_method] = $signature; $signature = ''; if ($include_doc_comments) { $signature .= "/**\n"; $signature .= " * Prepares the value of " . $column . " for output into HTML\n"; $signature .= " * \n"; $signature .= " * If the value is an fNumber object, the ->format() method will be called\n"; $signature .= " * resulting in the value including thousands separators\n"; $signature .= " * \n"; if ($type == 'float') { $signature .= " * @param integer \$decimal_places The number of decimal places to display - not passing any value or passing NULL will result in the intrisinc number of decimal places being shown\n"; } $signature .= " * @return string The HTML-ready value\n"; $signature .= " */\n"; } $prepare_method = 'prepare' . $camelized_column; $signature .= 'public function ' . $prepare_method . '('; if ($type == 'float') { $signature .= '$decimal_places=NULL'; } $signature .= ')'; $signatures[$prepare_method] = $signature; } } if (isset(self::$random_columns[$class])) { foreach (self::$random_columns[$class] as $column => $settings) { $signature = ''; if ($include_doc_comments) { $signature .= "/**\n"; $signature .= " * Generates a new random " . $settings['type'] . " character " . $settings['type'] . " string for " . $column . "\n"; $signature .= " * \n"; $signature .= " * If there is a UNIQUE constraint on the column and the value is not unique it will be regenerated until unique\n"; $signature .= " * \n"; $signature .= " * @return string The randomly generated string\n"; $signature .= " */\n"; } $generate_method = 'generate' . fGrammar::camelize($column, TRUE); $signature .= 'public function ' . $generate_method . '()'; $signatures[$generate_method] = $signature; } } }
/** * Counts the related records for all records in this set in one DB query * * @param string $related_class This should be the name of a related class * @param string $route This should be a column name or a join table name and is only required when there are multiple routes to a related table. If there are multiple routes and this is not specified, an fProgrammerException will be thrown. * @return fRecordSet The record set object, to allow for method chaining */ private function precount($related_class, $route = NULL) { if (!$this->records) { return $this; } $this->validateSingleClass('precount'); // If there are no primary keys we can just exit if (!array_merge($this->getPrimaryKeys())) { return $this; } $related_table = fORM::tablize($related_class); $table = fORM::tablize($this->class); $route = fORMSchema::getRouteName($table, $related_table, $route, '*-to-many'); $relationship = fORMSchema::getRoute($table, $related_table, $route, '*-to-many'); $table_with_route = $route ? $table . '{' . $route . '}' : $table; // Build the query out $where_sql = $this->constructWhereClause($route); $order_by_sql = $this->constructOrderByClause($route); $related_table_keys = fORMSchema::retrieve()->getKeys($related_table, 'primary'); $related_table_keys = fORMDatabase::addTableToValues($related_table, $related_table_keys); $related_table_keys = join(', ', $related_table_keys); $column = $table_with_route . '.' . $relationship['column']; $new_sql = 'SELECT count(' . $related_table_keys . ') AS __flourish_count, ' . $column . ' AS __flourish_column '; $new_sql .= ' FROM :from_clause '; $new_sql .= ' WHERE ' . $where_sql; $new_sql .= ' GROUP BY ' . $column; $new_sql .= ' ORDER BY ' . $column . ' ASC'; $new_sql = fORMDatabase::insertFromAndGroupByClauses($related_table, $new_sql); // Run the query and inject the results into the records $result = fORMDatabase::retrieve()->translatedQuery($new_sql); $counts = array(); foreach ($result as $row) { $counts[$row['__flourish_column']] = (int) $row['__flourish_count']; } unset($result); $total_records = sizeof($this->records); $get_method = 'get' . fGrammar::camelize($relationship['column'], TRUE); $tally_method = 'tally' . fGrammar::pluralize($related_class); for ($i = 0; $i < $total_records; $i++) { $record = $this->records[$i]; $count = isset($counts[$record->{$get_method}()]) ? $counts[$record->{$get_method}()] : 0; $record->{$tally_method}($count, $route); } 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 * * @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); } }
protected function setupORM() { if ($this->isOrmSettedUp) { return; } $this->defineConstants(); // Extract port from host. See wpdb::db_connect $port = null; $host = $this->wp->getDbHost(); if (preg_match('/^(.+):(\\d+)$/', trim($host), $m)) { $host = $m[1]; $port = $m[2]; } $database = new fDatabase('mysql', $this->wp->getDbName(), $this->wp->getDbUser(), $this->wp->getDbPassword(), $host, $port); // $database->enableDebugging(true); fORMDatabase::attach($database); fORM::mapClassToTable('WpTesting_Model_Test', WP_DB_PREFIX . 'posts'); fORM::mapClassToTable('WpTesting_Model_Question', WPT_DB_PREFIX . 'questions'); fORM::mapClassToTable('WpTesting_Model_Taxonomy', WP_DB_PREFIX . 'term_taxonomy'); fORM::mapClassToTable('WpTesting_Model_GlobalAnswer', WP_DB_PREFIX . 'terms'); fORM::mapClassToTable('WpTesting_Model_Answer', WPT_DB_PREFIX . 'answers'); fORM::mapClassToTable('WpTesting_Model_Scale', WP_DB_PREFIX . 'terms'); fORM::mapClassToTable('WpTesting_Model_Score', WPT_DB_PREFIX . 'scores'); fORM::mapClassToTable('WpTesting_Model_Passing', WPT_DB_PREFIX . 'passings'); fORM::mapClassToTable('WpTesting_Model_Result', WP_DB_PREFIX . 'terms'); fORM::mapClassToTable('WpTesting_Model_Formula', WPT_DB_PREFIX . 'formulas'); fORM::mapClassToTable('WpTesting_Model_Respondent', WP_DB_PREFIX . 'users'); fGrammar::addSingularPluralRule('Taxonomy', 'Taxonomy'); fGrammar::addSingularPluralRule('Score', 'Score'); fGrammar::addSingularPluralRule('Answer', 'Answer'); $schema = fORMSchema::retrieve('name:default'); $fkOptions = array('on_delete' => 'cascade', 'on_update' => 'cascade'); $schema->setKeysOverride(array(array('column' => 'test_id', 'foreign_table' => WP_DB_PREFIX . 'posts', 'foreign_column' => 'ID') + $fkOptions), WPT_DB_PREFIX . 'questions', 'foreign'); $schema->setKeysOverride(array(array('column' => 'answer_id', 'foreign_table' => WPT_DB_PREFIX . 'answers', 'foreign_column' => 'answer_id') + $fkOptions, array('column' => 'scale_id', 'foreign_table' => WP_DB_PREFIX . 'terms', 'foreign_column' => 'term_id') + $fkOptions), WPT_DB_PREFIX . 'scores', 'foreign'); $schema->setKeysOverride(array(array('column' => 'test_id', 'foreign_table' => WP_DB_PREFIX . 'posts', 'foreign_column' => 'ID') + $fkOptions, array('column' => 'respondent_id', 'foreign_table' => WP_DB_PREFIX . 'users', 'foreign_column' => 'ID') + $fkOptions), WPT_DB_PREFIX . 'passings', 'foreign'); $schema->setKeysOverride(array(array('column' => 'answer_id', 'foreign_table' => WPT_DB_PREFIX . 'answers', 'foreign_column' => 'answer_id') + $fkOptions, array('column' => 'passing_id', 'foreign_table' => WPT_DB_PREFIX . 'passings', 'foreign_column' => 'passing_id') + $fkOptions), WPT_DB_PREFIX . 'passing_answers', 'foreign'); $schema->setKeysOverride(array(array('column' => 'test_id', 'foreign_table' => WP_DB_PREFIX . 'posts', 'foreign_column' => 'ID') + $fkOptions, array('column' => 'result_id', 'foreign_table' => WP_DB_PREFIX . 'terms', 'foreign_column' => 'term_id') + $fkOptions), WPT_DB_PREFIX . 'formulas', 'foreign'); $schema->setColumnInfoOverride(null, WP_DB_PREFIX . 'term_relationships', 'term_order'); $schema->setKeysOverride(array(array('column' => 'object_id', 'foreign_table' => WP_DB_PREFIX . 'posts', 'foreign_column' => 'ID') + $fkOptions, array('column' => 'term_taxonomy_id', 'foreign_table' => WP_DB_PREFIX . 'term_taxonomy', 'foreign_column' => 'term_taxonomy_id') + $fkOptions), WP_DB_PREFIX . 'term_relationships', 'foreign'); $schema->setKeysOverride(array(array('column' => 'term_id', 'foreign_table' => WP_DB_PREFIX . 'terms', 'foreign_column' => 'term_id') + $fkOptions), WP_DB_PREFIX . 'term_taxonomy', 'foreign'); $schema->setKeysOverride(array(array('column' => 'question_id', 'foreign_table' => WPT_DB_PREFIX . 'questions', 'foreign_column' => 'question_id') + $fkOptions, array('column' => 'global_answer_id', 'foreign_table' => WP_DB_PREFIX . 'terms', 'foreign_column' => 'term_id') + $fkOptions), WPT_DB_PREFIX . 'answers', 'foreign'); $schema->setKeysOverride(array(), WPT_DB_PREFIX . 'sections', 'foreign'); $schema->setKeysOverride(array(), WPT_DB_PREFIX . 'fields', 'foreign'); $schema->setKeysOverride(array(), WPT_DB_PREFIX . 'field_values', 'foreign'); $this->wp->doAction('wp_testing_orm_setup', $schema, $database); $this->isOrmSettedUp = true; }
/** * Takes a scalar value and turns it into an object if applicable * * @internal * * @param string $class The class name of the class the column is part of * @param string $column The database column * @param mixed $value The value to possibly objectify * @return mixed The scalar or object version of the value, depending on the column type and column options */ public static function objectify($class, $column, $value) { // This short-circuits computation for already checked columns, providing // a nice little performance boost to pages with lots of records if (isset(self::$cache['objectify'][$class . '::' . $column])) { return $value; } if (!empty(self::$objectify_callbacks[$class][$column])) { return call_user_func(self::$objectify_callbacks[$class][$column], $class, $column, $value); } $table = self::tablize($class); $schema = fORMSchema::retrieve($class); // Turn date/time values into objects $column_type = $schema->getColumnInfo($table, $column, 'type'); if (in_array($column_type, array('date', 'time', 'timestamp'))) { if ($value === NULL) { return $value; } try { // Explicit calls to the constructors are used for dependency detection switch ($column_type) { case 'date': $value = new fDate($value); break; case 'time': $value = new fTime($value); break; case 'timestamp': $value = new fTimestamp($value); break; } } catch (fValidationException $e) { // Validation exception results in the raw value being saved } } else { self::$cache['objectify'][$class . '::' . $column] = TRUE; } return $value; }
/** * Sets a column to be a file upload column * * Configuring a column to be a file upload column means that whenever * fActiveRecord::populate() is called for an fActiveRecord object, any * appropriately named file uploads (via `$_FILES`) will be moved into * the directory for this column. * * Setting the column to a file path will cause the specified file to * be copied into the directory for this column. * * @param mixed $class The class name or instance of the class * @param string $column The column to set as a file upload column * @param fDirectory|string $directory The directory to upload/move to * @return void */ public static function configureFileUploadColumn($class, $column, $directory) { $class = fORM::getClass($class); $table = fORM::tablize($class); $schema = fORMSchema::retrieve($class); $data_type = $schema->getColumnInfo($table, $column, 'type'); $valid_data_types = array('varchar', 'char', 'text'); if (!in_array($data_type, $valid_data_types)) { throw new fProgrammerException('The column specified, %1$s, is a %2$s column. Must be one of %3$s to be set as a file upload column.', $column, $data_type, join(', ', $valid_data_types)); } if (!is_object($directory)) { $directory = new fDirectory($directory); } if (!$directory->isWritable()) { throw new fEnvironmentException('The file upload directory, %s, is not writable', $directory->getPath()); } $camelized_column = fGrammar::camelize($column, TRUE); fORM::registerActiveRecordMethod($class, 'upload' . $camelized_column, self::upload); fORM::registerActiveRecordMethod($class, 'set' . $camelized_column, self::set); fORM::registerActiveRecordMethod($class, 'encode' . $camelized_column, self::encode); fORM::registerActiveRecordMethod($class, 'prepare' . $camelized_column, self::prepare); fORM::registerReflectCallback($class, self::reflect); fORM::registerInspectCallback($class, $column, self::inspect); fORM::registerReplicateCallback($class, $column, self::replicate); fORM::registerObjectifyCallback($class, $column, self::objectify); $only_once_hooks = array('post-begin::delete()' => self::begin, 'pre-commit::delete()' => self::delete, 'post-commit::delete()' => self::commit, 'post-rollback::delete()' => self::rollback, 'post::populate()' => self::populate, 'post-begin::store()' => self::begin, 'post-validate::store()' => self::moveFromTemp, 'pre-commit::store()' => self::deleteOld, 'post-commit::store()' => self::commit, 'post-rollback::store()' => self::rollback, 'post::validate()' => self::validate); foreach ($only_once_hooks as $hook => $callback) { if (!fORM::checkHookCallback($class, $hook, $callback)) { fORM::registerHookCallback($class, $hook, $callback); } } if (empty(self::$file_upload_columns[$class])) { self::$file_upload_columns[$class] = array(); } self::$file_upload_columns[$class][$column] = $directory; }
/** * Validates one-to-* related records * * @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 * @param string $related_class The name of the class for this record set * @param string $route The route between the table and related table * @return array An array of validation messages */ private static function validateOneToStar($class, &$values, &$related_records, $related_class, $route) { $schema = fORMSchema::retrieve($class); $table = fORM::tablize($class); $related_table = fORM::tablize($related_class); $first_pk_column = self::determineFirstPKColumn($class, $related_class, $route); $filter = self::determineRequestFilter($class, $related_class, $route); $pk_field = $filter . $first_pk_column; $input_keys = array_keys(fRequest::get($pk_field, 'array', array())); $related_record_name = self::getRelatedRecordName($class, $related_class, $route); $messages = array(); $one_to_one = fORMSchema::isOneToOne($schema, $table, $related_table, $route); if ($one_to_one) { $records = array(self::createRecord($class, $values, $related_records, $related_class, $route)); } else { $records = self::buildRecords($class, $values, $related_records, $related_class, $route); } // Ignore validation messages about the primary key since it will be added $primary_key_name = fValidationException::formatField(fORM::getColumnName($related_class, $route)); $primary_key_regex = '#^' . preg_quote($primary_key_name, '#') . '.*$#D'; fORMValidation::addRegexReplacement($related_class, $primary_key_regex, ''); foreach ($records as $i => $record) { fRequest::filter($filter, isset($input_keys[$i]) ? $input_keys[$i] : $i); $record_messages = $record->validate(TRUE); foreach ($record_messages as $record_message) { $token_field = fValidationException::formatField('__TOKEN__'); $extract_message_regex = '#' . str_replace('__TOKEN__', '(.*?)', preg_quote($token_field, '#')) . '(.*)$#D'; preg_match($extract_message_regex, $record_message, $matches); if ($one_to_one) { $column_name = self::compose('%1$s %2$s', $related_record_name, $matches[1]); } else { $column_name = self::compose('%1$s #%2$s %3$s', $related_record_name, $i + 1, $matches[1]); } $messages[] = self::compose('%1$s%2$s', fValidationException::formatField($column_name), $matches[2]); } fRequest::unfilter(); } fORMValidation::removeRegexReplacement($related_class, $primary_key_regex, ''); return $messages; }