public function testPrebuildOneToManyMultiColumn() { fORMRelated::setOrderBys('User', 'FavoriteAlbum', array('album_id' => 'desc')); $set = fRecordSet::build('User'); $set->prebuildFavoriteAlbums(); ob_start(); fORMDatabase::retrieve()->enableDebugging(TRUE); foreach ($set as $user) { $primary_keys = $user->listFavoriteAlbums(); switch ($user->getUserId()) { case 1: $expected_primary_keys = array(array('email' => '*****@*****.**', 'album_id' => 7), array('email' => '*****@*****.**', 'album_id' => 4), array('email' => '*****@*****.**', 'album_id' => 3), array('email' => '*****@*****.**', 'album_id' => 2), array('email' => '*****@*****.**', 'album_id' => 1)); break; case 2: $expected_primary_keys = array(array('email' => '*****@*****.**', 'album_id' => 2)); break; case 3: $expected_primary_keys = array(); break; case 4: $expected_primary_keys = array(); break; } $this->assertEquals($expected_primary_keys, $primary_keys); } fORMDatabase::retrieve()->enableDebugging(FALSE); $output = ob_get_clean(); $this->assertEquals('', $output); }
/** * 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 an `ORDER BY` clause to the SQL for the primary keys of this record set * * @param fDatabase $db The database the query will be executed on * @param fSchema $schema The schema for the database * @param array $params The parameters for the fDatabase::query() call * @param string $related_class The related class to add the order bys for * @param string $route The route to this table from another table * @return array The params with the `ORDER BY` clause added */ private function addOrderByParams($db, $schema, $params, $related_class, $route = NULL) { $table = fORM::tablize($this->class); $table_with_route = $route ? $table . '{' . $route . '}' : $table; $pk_columns = $schema->getKeys($table, 'primary'); $first_pk_column = $pk_columns[0]; $escaped_pk_columns = array(); foreach ($pk_columns as $pk_column) { $escaped_pk_columns[$pk_column] = $db->escape('%r', $table_with_route . '.' . $pk_column); } $column_info = $schema->getColumnInfo($table); $sql = ''; $number = 0; foreach ($this->getPrimaryKeys() as $primary_key) { $sql .= 'WHEN '; if (is_array($primary_key)) { $conditions = array(); foreach ($pk_columns as $pk_column) { $value = $primary_key[$pk_column]; // This makes sure the query performs the way an insert will if ($value === NULL && $column_info[$pk_column]['not_null'] && $column_info[$pk_column]['default'] !== NULL) { $value = $column_info[$pk_column]['default']; } $conditions[] = str_replace('%r', $escaped_pk_columns[$pk_column], fORMDatabase::makeCondition($schema, $table, $pk_column, '=', $value)); $params[] = $value; } $sql .= join(' AND ', $conditions); } else { $sql .= str_replace('%r', $escaped_pk_columns[$first_pk_column], fORMDatabase::makeCondition($schema, $table, $first_pk_column, '=', $primary_key)); $params[] = $primary_key; } $sql .= ' THEN ' . $number . ' '; $number++; } $params[0] .= 'CASE ' . $sql . 'END ASC'; if ($related_order_bys = fORMRelated::getOrderBys($this->class, $related_class, $route)) { $params[0] .= ', '; $params = fORMDatabase::addOrderByClause($db, $schema, $params, fORM::tablize($related_class), $related_order_bys); } return $params; }
/** * Stores any many-to-many associations or any one-to-many records that have been flagged for association * * @internal * * @param string $class The class to store the related records for * @param array &$values The current values for the main record being stored * @param array &$related_records The related records array * @param boolean $force_cascade This flag will be passed to the fActiveRecord::delete() method on related records that are being deleted * @return void */ public static function store($class, &$values, &$related_records, $force_cascade) { $table = fORM::tablize($class); $schema = fORMSchema::retrieve($class); foreach ($related_records as $related_table => $relationships) { foreach ($relationships as $route => $related_info) { if (!$related_info['associate']) { continue; } $relationship = fORMSchema::getRoute($schema, $table, $related_table, $route); if (isset($relationship['join_table'])) { fORMRelated::storeManyToMany($class, $values, $relationship, $related_info); } else { $related_class = fORM::classize($related_table); $related_class = fORM::getRelatedClass($class, $related_class); fORMRelated::storeOneToStar($class, $values, $related_records, $related_class, $route, $force_cascade); } } } }
public function testChildValidationName() { try { fORMOrdering::configureOrderingColumn('FavoriteAlbum', 'position'); fORMRelated::registerValidationNameMethod('User', 'FavoriteAlbum', 'makeName'); $user = $this->createUser(); $favorite_album_1 = new FavoriteAlbum(); $favorite_album_2 = new FavoriteAlbum(); $user->associateFavoriteAlbums(array($favorite_album_1, $favorite_album_2)); $user->validate(); } catch (fValidationException $e) { $message = preg_replace('#\\s+#', ' ', strip_tags($e->getMessage())); $this->assertContains('The following problems were found: Album 1 Album ID: Please enter a value Album 2 Album ID: Please enter a value', $message); } }
/** * Validates related records for an fActiveRecord object * * @internal * * @param fActiveRecord $object The object to validate * @param array &$values The values for the object * @param array &$related_records The related records for the object * @return array An array of messages */ public static function validateRelated($object, &$values, &$related_records) { $class = get_class($object); $table = fORM::tablize($class); $validation_messages = array(); // Check related rules foreach (self::$related_one_or_more_rules[$class] as $related_class => $routes) { foreach ($routes as $route => $enabled) { $message = self::checkRelatedOneOrMoreRule($object, $values, $related_records, $related_class, $route); if ($message) { $validation_messages[fORM::tablize($related_class)] = $message; } } } $related_messages = fORMRelated::validate($class, $values, $related_records); $validation_messages = array_merge($validation_messages, $related_messages); return $validation_messages; }
protected function configure() { fORMRelated::setOrderBys($this, 'WpTesting_Model_Taxonomy', array(WP_DB_PREFIX . 'term_relationships.`term_order`' => 'asc')); }
public function testPrebuildOneToMany() { fORMRelated::setOrderBys('Flourish2Artist', 'Flourish2Album', array('album_id' => 'desc')); $set = fRecordSet::build('Flourish2Artist'); $set->prebuildFlourish2Albums(); ob_start(); fORMDatabase::retrieve()->enableDebugging(TRUE); foreach ($set as $artist) { $album_ids = $artist->listFlourish2Albums(); switch ($artist->getArtistId()) { case 1: $expected_album_ids = array(3, 2, 1); break; case 2: $expected_album_ids = array(5, 4); break; case 3: $expected_album_ids = array(6); break; } $this->assertEquals($expected_album_ids, $album_ids); } fORMDatabase::retrieve()->enableDebugging(FALSE); $output = ob_get_clean(); $this->assertEquals('', $output); }
/** * Associate many records with it's related records by foreign key in one query * @param fRecordset|array $records * @param string $relatedClassName * @param string $foreignKeyName * @return array Objects fo type relatedClassName by it's ids */ protected function associateManyRelated($records, $relatedClassName, $foreignKeyName) { $recordsById = array(); if ($records instanceof fRecordSet) { foreach ($records as $record) { $recordsById[$record->getId()] = $record; } } else { $recordsById = $records; } if (empty($recordsById)) { return array(); } // Get related records $orderBys = fORMRelated::getOrderBys(get_class(reset($recordsById)), $relatedClassName, $foreignKeyName); $relatedRecords = fRecordSet::build($relatedClassName, array($foreignKeyName . '=' => array_keys($recordsById)), $orderBys); $relatedRecordsById = array(); $relatedRecordsByByForeignKey = array(); foreach ($relatedRecords as $relatedRecord) { $relatedRecordsById[$relatedRecord->getId()] = $relatedRecord; $relatedRecordsByByForeignKey[$relatedRecord->get($foreignKeyName)][] = $relatedRecord; } // Assoc related records to records $associateMethodName = 'associate' . $relatedClassName; foreach ($relatedRecordsByByForeignKey as $foreignKeyValue => $relatedRecords) { $recordsById[$foreignKeyValue]->{$associateMethodName}($relatedRecords); } return $relatedRecordsById; }
/** * @dataProvider listManyToManyProvider */ public function testListManyToMany($user_id, $list) { fORMRelated::setOrderBys('User', 'Group', array('group_id' => 'asc'), 'users_groups'); $user = new User($user_id); $this->assertEquals($list, $user->listGroups('users_groups')); }
protected function configure() { fORMRelated::setOrderBys($this,'Subscription',array('subscriptions.subscription_id' => 'desc')); }
/** * Builds 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 prebuild($related_class, $route = NULL) { if (!$this->records) { return $this; } $this->validateSingleClass('prebuild'); // 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); if ($related_order_bys = fORMRelated::getOrderBys($this->class, $related_class, $route)) { $order_by_sql .= ', ' . fORMDatabase::createOrderByClause($related_table, $related_order_bys); } $new_sql = 'SELECT ' . $related_table . '.*'; // If we are going through a join table we need the related primary key for matching if (isset($relationship['join_table'])) { $new_sql .= ", " . $table_with_route . '.' . $relationship['column']; } $new_sql .= ' FROM :from_clause '; $new_sql .= ' WHERE ' . $where_sql; $new_sql .= ' :group_by_clause '; $new_sql .= ' ORDER BY ' . $order_by_sql; $new_sql = fORMDatabase::insertFromAndGroupByClauses($related_table, $new_sql); // Add the joining column to the group by if (strpos($new_sql, 'GROUP BY') !== FALSE) { $new_sql = str_replace(' ORDER BY', ', ' . $table . '.' . $relationship['column'] . ' ORDER BY', $new_sql); } // Run the query and inject the results into the records $result = fORMDatabase::retrieve()->translatedQuery($new_sql); $total_records = sizeof($this->records); for ($i = 0; $i < $total_records; $i++) { // Get the record we are injecting into $record = $this->records[$i]; $keys = array(); // If we are going through a join table, keep track of the record by the value in the join table if (isset($relationship['join_table'])) { try { $current_row = $result->current(); $keys[$relationship['column']] = $current_row[$relationship['column']]; } catch (fExpectedException $e) { } // If it is a straight join, keep track of the value by the related column value } else { $method = 'get' . fGrammar::camelize($relationship['related_column'], TRUE); $keys[$relationship['related_column']] = $record->{$method}(); } // Loop through and find each row for the current record $rows = array(); try { while (!array_diff_assoc($keys, $result->current())) { $row = $result->fetchRow(); // If we are going through a join table we need to remove the related primary key that was used for matching if (isset($relationship['join_table'])) { unset($row[$relationship['column']]); } $rows[] = $row; } } catch (fExpectedException $e) { } // Set up the result object for the new record set $set = new fRecordSet($related_class, new ArrayIterator($rows)); // Inject the new record set into the record $method = 'inject' . fGrammar::pluralize($related_class); $record->{$method}($set, $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; }
protected function configure() { fORMRelated::setOrderBys($this, 'WpTesting_Model_Answer', array(WPT_DB_PREFIX . 'answers.answer_sort' => 'asc', WPT_DB_PREFIX . 'answers.answer_id' => 'asc')); }
/** * Validates many-to-many related records * * @param string $class The class to validate the related records for * @param string $related_class The name of the class for this record set * @param string $route The route between the table and related table * @param array $related_info The related info to validate * @return array An array of validation messages */ private static function validateManyToMany($class, $related_class, $route, $related_info) { $related_record_name = fORMRelated::getRelatedRecordName($class, $related_class, $route); $record_number = 1; $messages = array(); $related_records = $related_info['record_set'] ? $related_info['record_set'] : $related_info['primary_keys']; foreach ($related_records as $record) { if (is_object($record) && !$record->exists() || !$record) { $messages[] = self::compose('%sPlease select a %3$s', fValidationException::formatField(self::compose('%1$s #%2$s', $related_record_name, $record_number)), $related_record_name); } $record_number++; } return $messages; }