diff --git a/model/Versioned.php b/model/Versioned.php index a2a457a4c..8c1dcabc6 100644 --- a/model/Versioned.php +++ b/model/Versioned.php @@ -1,11 +1,15 @@ "Int" ); + /** + * @var array + */ + private static $db = array( + 'Version' => 'Int' + ); + + /** + * Keep track of the archive tables that have been created. + * + * @var array + */ + private static $archive_tables = array(); + /** * Additional database indexes for the new * "_versions" table. Used in {@link augmentDatabase()}. @@ -78,7 +101,25 @@ class Versioned extends DataExtension { ); /** - * Reset static configuration variables to their default values + * An array of DataObject extensions that may require versioning for extra tables + * The array value is a set of suffixes to form these table names, assuming a preceding '_'. + * E.g. if Extension1 creates a new table 'Class_suffix1' + * and Extension2 the tables 'Class_suffix2' and 'Class_suffix3': + * + * $versionableExtensions = array( + * 'Extension1' => 'suffix1', + * 'Extension2' => array('suffix2', 'suffix3'), + * ); + * + * Make sure your extension has a static $enabled-property that determines if it is + * processed by Versioned. + * + * @var array + */ + protected static $versionableExtensions = array('Translatable' => 'lang'); + + /** + * Reset static configuration variables to their default values. */ public static function reset() { self::$reading_mode = ''; @@ -88,36 +129,46 @@ class Versioned extends DataExtension { /** * Construct a new Versioned object. + * * @var array $stages The different stages the versioned object can be. - * The first stage is consiedered the 'default' stage, the last stage is + * The first stage is considered the 'default' stage, the last stage is * considered the 'live' stage. */ - public function __construct($stages=array('Stage','Live')) { + public function __construct($stages = array('Stage','Live')) { parent::__construct(); if(!is_array($stages)) { $stages = func_get_args(); } + $this->stages = $stages; $this->defaultStage = reset($stages); $this->liveStage = array_pop($stages); } - private static $db = array( - 'Version' => 'Int' - ); - + /** + * @param string + * @param string + * @param array + * + * @return array + */ public static function get_extra_config($class, $extension, $args) { - array( + return array( 'has_many' => array('Versions' => $class) ); } /** - * Amend freshly created DataQuery objects with versioned-specific information + * Amend freshly created DataQuery objects with versioned-specific + * information. + * + * @param SQLQuery + * @param DataQuery */ public function augmentDataQueryCreation(SQLQuery &$query, DataQuery &$dataQuery) { $parts = explode('.', Versioned::get_reading_mode()); + if($parts[0] == 'Archive') { $dataQuery->setQueryParam('Versioned.mode', 'archive'); $dataQuery->setQueryParam('Versioned.date', $parts[1]); @@ -277,12 +328,13 @@ class Versioned extends DataExtension { } /** - * For lazy loaded fields requiring extra sql manipulation, ie versioning + * For lazy loaded fields requiring extra sql manipulation, ie versioning. + * * @param SQLQuery $query * @param DataQuery $dataQuery * @param DataObject $dataObject */ - function augmentLoadLazyFields(SQLQuery &$query, DataQuery &$dataQuery = null, $dataObject) { + public function augmentLoadLazyFields(SQLQuery &$query, DataQuery &$dataQuery = null, $dataObject) { // The VersionedMode local variable ensures that this decorator only applies to // queries that have originated from the Versioned object, and have the Versioned // metadata set on the query object. This prevents regular queries from @@ -302,14 +354,11 @@ class Versioned extends DataExtension { $dataQuery->where("\"$dataClass\".\"ID\" = {$dataObject->ID}")->limit(1); } } - - /** - * Keep track of the archive tables that have been created - */ - private static $archive_tables = array(); + /** * Called by {@link SapphireTest} when the database is reset. + * * @todo Reduce the coupling between this and SapphireTest, somehow. */ public static function on_db_reset() { @@ -323,24 +372,6 @@ class Versioned extends DataExtension { // Remove references to them self::$archive_tables = array(); } - - /** - * An array of DataObject extensions that may require versioning for extra tables - * The array value is a set of suffixes to form these table names, assuming a preceding '_'. - * E.g. if Extension1 creates a new table 'Class_suffix1' - * and Extension2 the tables 'Class_suffix2' and 'Class_suffix3': - * - * $versionableExtensions = array( - * 'Extension1' => 'suffix1', - * 'Extension2' => array('suffix2', 'suffix3'), - * ); - * - * Make sure your extension has a static $enabled-property that determines if it is - * processed by Versioned. - * - * @var array - */ - protected static $versionableExtensions = array('Translatable' => 'lang'); public function augmentDatabase() { $classTable = $this->owner->class; @@ -507,6 +538,7 @@ class Versioned extends DataExtension { /** * Augment a write-record request. + * * @param SQLQuery $manipulation Query to augment. */ public function augmentWrite(&$manipulation) { @@ -602,10 +634,15 @@ class Versioned extends DataExtension { } // Clear the migration flag - if($this->migratingVersion) $this->migrateVersion(null); + if($this->migratingVersion) { + $this->migrateVersion(null); + } - // Add the new version # back into the data object, for accessing after this write - if(isset($thisVersion)) $this->owner->Version = str_replace("'","",$thisVersion); + // Add the new version # back into the data object, for accessing + // after this write + if(isset($thisVersion)) { + $this->owner->Version = str_replace("'","", $thisVersion); + } } /** @@ -616,23 +653,30 @@ class Versioned extends DataExtension { */ public function writeWithoutVersion() { $this->_nextWriteWithoutVersion = true; + return $this->owner->write(); } + /** + * + */ public function onAfterWrite() { $this->_nextWriteWithoutVersion = false; } /** - * If a write was skipped, then we need to ensure that we don't leave a migrateVersion() - * value lying around for the next write. + * If a write was skipped, then we need to ensure that we don't leave a + * migrateVersion() value lying around for the next write. + * + * */ public function onAfterSkippedWrite() { $this->migrateVersion(null); } /** - * Determine if a table is supporting the Versioned extensions (e.g. $table_versions does exists) + * Determine if a table is supporting the Versioned extensions (e.g. + * $table_versions does exists). * * @param string $table Table name * @return boolean @@ -644,20 +688,29 @@ class Versioned extends DataExtension { } /** - * Check if a certain table has the 'Version' field + * Check if a certain table has the 'Version' field. * * @param string $table Table name + * * @return boolean Returns false if the field isn't in the table, true otherwise */ public function hasVersionField($table) { $rPos = strrpos($table,'_'); + if(($rPos !== false) && in_array(substr($table,$rPos), $this->stages)) { $tableWithoutStage = substr($table,0,$rPos); } else { $tableWithoutStage = $table; } + return ('DataObject' == get_parent_class($tableWithoutStage)); } + + /** + * @param string $table + * + * @return string + */ public function extendWithSuffix($table) { foreach (Versioned::$versionableExtensions as $versionableExtension => $suffixes) { if ($this->owner->hasExtension($versionableExtension)) { @@ -667,13 +720,13 @@ class Versioned extends DataExtension { $ext->clearOwner(); } } + return $table; } - //-----------------------------------------------------------------------------------------------// - /** * Get the latest published DataObject. + * * @return DataObject */ public function latestPublished() { @@ -690,6 +743,7 @@ class Versioned extends DataExtension { /** * Move a database record from one stage to the other. + * * @param fromStage Place to copy from. Can be either a stage name or a version number. * @param toStage Place to copy to. Must be a stage name. * @param createNewVersion Set this to true to create a new version number. By default, the existing version @@ -741,6 +795,7 @@ class Versioned extends DataExtension { /** * Set the migrating version. + * * @param string $version The version. */ public function migrateVersion($version) { @@ -749,7 +804,9 @@ class Versioned extends DataExtension { /** * Compare two stages to see if they're different. + * * Only checks the version numbers, not the actual content. + * * @param string $stage1 The first stage to check. * @param string $stage2 */ @@ -761,11 +818,14 @@ class Versioned extends DataExtension { return true; } - // We test for equality - if one of the versions doesn't exist, this will be false - //TODO: DB Abstraction: if statement here: + // We test for equality - if one of the versions doesn't exist, this + // will be false. + + // TODO: DB Abstraction: if statement here: $stagesAreEqual = DB::query("SELECT CASE WHEN \"$table1\".\"Version\"=\"$table2\".\"Version\"" . " THEN 1 ELSE 0 END FROM \"$table1\" INNER JOIN \"$table2\" ON \"$table1\".\"ID\" = \"$table2\".\"ID\"" . " AND \"$table1\".\"ID\" = {$this->owner->ID}")->value(); + return !$stagesAreEqual; } @@ -833,8 +893,10 @@ class Versioned extends DataExtension { /** * Compare two version, and return the diff between them. + * * @param string $from The version to compare from. * @param string $to The version to compare to. + * * @return DataObject */ public function compareVersions($from, $to) { @@ -842,26 +904,39 @@ class Versioned extends DataExtension { $toRecord = Versioned::get_version($this->owner->class, $this->owner->ID, $to); $diff = new DataDifferencer($fromRecord, $toRecord); + return $diff->diffedData(); } /** * Return the base table - the class that directly extends DataObject. + * * @return string */ public function baseTable($stage = null) { $tableClasses = ClassInfo::dataClassesFor($this->owner->class); $baseClass = array_shift($tableClasses); - return (!$stage || $stage == $this->defaultStage) ? $baseClass : $baseClass . "_$stage"; + + if(!$stage || $stage == $this->defaultStage) { + return $baseClass; + } + + return $baseClass . "_$stage"; } //-----------------------------------------------------------------------------------------------// /** * Choose the stage the site is currently on. - * If $_GET['stage'] is set, then it will use that stage, and store it in the session. - * if $_GET['archiveDate'] is set, it will use that date, and store it in the session. - * If neither of these are set, it checks the session, otherwise the stage is set to 'Live'. + * + * If $_GET['stage'] is set, then it will use that stage, and store it in + * the session. + * + * if $_GET['archiveDate'] is set, it will use that date, and store it in + * the session. + * + * If neither of these are set, it checks the session, otherwise the stage + * is set to 'Live'. */ public static function choose_site_stage() { if(isset($_GET['stage'])) { @@ -900,6 +975,8 @@ class Versioned extends DataExtension { /** * Set the current reading mode. + * + * @param string $mode */ public static function set_reading_mode($mode) { Versioned::$reading_mode = $mode; @@ -907,6 +984,7 @@ class Versioned extends DataExtension { /** * Get the current reading mode. + * * @return string */ public static function get_reading_mode() { @@ -915,6 +993,7 @@ class Versioned extends DataExtension { /** * Get the name of the 'live' stage. + * * @return string */ public static function get_live_stage() { @@ -923,15 +1002,20 @@ class Versioned extends DataExtension { /** * Get the current reading stage. + * * @return string */ public static function current_stage() { $parts = explode('.', Versioned::get_reading_mode()); - if($parts[0] == 'Stage') return $parts[1]; + + if($parts[0] == 'Stage') { + return $parts[1]; + } } /** * Get the current archive date. + * * @return string */ public static function current_archived_date() { @@ -941,6 +1025,7 @@ class Versioned extends DataExtension { /** * Set the reading stage. + * * @param string $stage New reading stage. */ public static function reading_stage($stage) { @@ -949,6 +1034,7 @@ class Versioned extends DataExtension { /** * Set the reading archive date. + * * @param string $date New reading archived date. */ public static function reading_archived_date($date) { @@ -964,11 +1050,13 @@ class Versioned extends DataExtension { * @param string $filter A filter to be inserted into the WHERE clause. * @param boolean $cache Use caching. * @param string $orderby A sort expression to be inserted into the ORDER BY clause. + * * @return DataObject */ public static function get_one_by_stage($class, $stage, $filter = '', $cache = true, $sort = '') { // TODO: No identity cache operating $items = self::get_by_stage($class, $stage, $filter, $sort, null, 1); + return $items->First(); } @@ -979,6 +1067,7 @@ class Versioned extends DataExtension { * @param string $stage * @param int $id * @param boolean $cache + * * @return int */ public static function get_versionnumber_by_stage($class, $stage, $id, $cache = true) { @@ -995,10 +1084,14 @@ class Versioned extends DataExtension { // cache value (if required) if($cache) { - if(!isset(self::$cache_versionnumber[$baseClass])) self::$cache_versionnumber[$baseClass] = array(); + if(!isset(self::$cache_versionnumber[$baseClass])) { + self::$cache_versionnumber[$baseClass] = array(); + } + if(!isset(self::$cache_versionnumber[$baseClass][$stage])) { self::$cache_versionnumber[$baseClass][$stage] = array(); } + self::$cache_versionnumber[$baseClass][$stage][$id] = $version; } @@ -1006,17 +1099,26 @@ class Versioned extends DataExtension { } /** - * Pre-populate the cache for Versioned::get_versionnumber_by_stage() for a list of record IDs, - * for more efficient database querying. If $idList is null, then every page will be pre-cached. + * Pre-populate the cache for Versioned::get_versionnumber_by_stage() for + * a list of record IDs, for more efficient database querying. If $idList + * is null, then every page will be pre-cached. + * + * @param string $class + * @param string $stage + * @param array $idList */ public static function prepopulate_versionnumber_cache($class, $stage, $idList = null) { $filter = ""; + if($idList) { // Validate the ID list - foreach($idList as $id) if(!is_numeric($id)) { - user_error("Bad ID passed to Versioned::prepopulate_versionnumber_cache() in \$idList: " . $id, + foreach($idList as $id) { + if(!is_numeric($id)) { + user_error("Bad ID passed to Versioned::prepopulate_versionnumber_cache() in \$idList: " . $id, E_USER_ERROR); + } } + $filter = "WHERE \"ID\" IN(" .implode(", ", $idList) . ")"; } @@ -1024,6 +1126,7 @@ class Versioned extends DataExtension { $stageTable = ($stage == 'Stage') ? $baseClass : "{$baseClass}_{$stage}"; $versions = DB::query("SELECT \"ID\", \"Version\" FROM \"$stageTable\" $filter")->map(); + foreach($versions as $id => $version) { self::$cache_versionnumber[$baseClass][$stage][$id] = $version; } @@ -1039,6 +1142,7 @@ class Versioned extends DataExtension { * @param string $join Deprecated, use leftJoin($table, $joinClause) instead * @param int $limit A limit on the number of records returned from the database. * @param string $containerClass The container class for the result set (default is DataList) + * * @return SS_List */ public static function get_by_stage($class, $stage, $filter = '', $sort = '', $join = '', $limit = '', @@ -1051,6 +1155,11 @@ class Versioned extends DataExtension { )); } + /** + * @param string $stage + * + * @return int + */ public function deleteFromStage($stage) { $oldMode = Versioned::get_reading_mode(); Versioned::reading_stage($stage); @@ -1065,11 +1174,17 @@ class Versioned extends DataExtension { return $result; } + /** + * @param string $stage + * @param boolean $forceInsert + */ public function writeToStage($stage, $forceInsert = false) { $oldMode = Versioned::get_reading_mode(); Versioned::reading_stage($stage); + $result = $this->owner->write(false, $forceInsert); Versioned::set_reading_mode($oldMode); + return $result; } @@ -1077,7 +1192,7 @@ class Versioned extends DataExtension { * Roll the draft version of this page to match the published page. * Caution: Doesn't overwrite the object properties with the rolled back version. * - * @param $version Either the string 'Live' or a version number + * @param int $version Either the string 'Live' or a version number */ public function doRollbackTo($version) { $this->owner->extend('onBeforeRollback', $version); @@ -1110,7 +1225,7 @@ class Versioned extends DataExtension { * @see get_latest_version() * @see latestPublished * - * @return bool + * @return boolean */ public function isLatestVersion() { $version = self::get_latest_version($this->owner->class, $this->owner->ID); @@ -1123,6 +1238,10 @@ class Versioned extends DataExtension { * version of each page stored in the (class)_versions tables. * * In particular, this will query deleted records as well as active ones. + * + * @param string $class + * @param string $filter + * @param string $sort */ public static function get_including_deleted($class, $filter = "", $sort = "") { $list = DataList::create($class) @@ -1135,9 +1254,15 @@ class Versioned extends DataExtension { /** * Return the specific version of the given id. - * Caution: The record is retrieved as a DataObject, but saving back modifications - * via write() will create a new version, rather than modifying the existing one. + * + * Caution: The record is retrieved as a DataObject, but saving back + * modifications via write() will create a new version, rather than + * modifying the existing one. * + * @param string $class + * @param int $id + * @param int $version + * * @return DataObject */ public static function get_version($class, $id, $version) { @@ -1151,7 +1276,11 @@ class Versioned extends DataExtension { } /** - * Return a list of all versions for a given id + * Return a list of all versions for a given id. + * + * @param string $class + * @param int $id + * * @return DataList */ public static function get_all_versions($class, $id) { @@ -1163,25 +1292,44 @@ class Versioned extends DataExtension { return $list; } + /** + * @param Controller $controller + */ public function contentcontrollerInit($controller) { self::choose_site_stage(); } + + /** + * @param Controller $controller + */ public function modelascontrollerInit($controller) { self::choose_site_stage(); } - protected static $reading_mode = null; - + /** + * @param array $labels + */ public function updateFieldLabels(&$labels) { $labels['Versions'] = _t('Versioned.has_many_Versions', 'Versions', 'Past Versions of this page'); } + /** + * @param FieldList + */ + public function updateCMSFields(&$fields) { + // remove the version field from the CMS as this should be left + // entirely up to the extension (not the cms user). + $fields->removeByName('Version'); + } + public function flushCache() { self::$cache_versionnumber = array(); } /** - * Return a piece of text to keep DataObject cache keys appropriately specific + * Return a piece of text to keep DataObject cache keys appropriately specific. + * + * @return string */ public function cacheKeyComponent() { return 'versionedmode-'.self::get_reading_mode(); @@ -1197,7 +1345,14 @@ class Versioned extends DataExtension { * @see Versioned */ class Versioned_Version extends ViewableData { + /** + * @var array + */ protected $record; + + /** + * @var DataObject + */ protected $object; public function __construct($record) { @@ -1211,23 +1366,36 @@ class Versioned_Version extends ViewableData { parent::__construct(); } + /** + * @return string + */ public function PublishedClass() { return $this->record['WasPublished'] ? 'published' : 'internal'; } + /** + * @return Member + */ public function Author() { - return DataObject::get_by_id("Member", $this->record['AuthorID']); + return Member::get()->byId($this->record['AuthorID']); } + /** + * @return Member + */ public function Publisher() { - if( !$this->record['WasPublished'] ) + if (!$this->record['WasPublished']) { return null; + } - return DataObject::get_by_id("Member", $this->record['PublisherID']); + return Member::get()->byId($this->record['PublisherID']); } + /** + * @return boolean + */ public function Published() { - return !empty( $this->record['WasPublished'] ); + return !empty($this->record['WasPublished']); } /** @@ -1243,17 +1411,23 @@ class Versioned_Version extends ViewableData { // Traverse dot syntax foreach($parts as $relation) { if($component instanceof SS_List) { - if(method_exists($component,$relation)) $component = $component->$relation(); - else $component = $component->relation($relation); + if(method_exists($component,$relation)) { + $component = $component->$relation(); + } else { + $component = $component->relation($relation); + } } else { $component = $component->$relation(); -} + } } } // Unlike has-one's, these "relations" can return false if($component) { - if ($component->hasMethod($fieldName)) return $component->$fieldName(); + if ($component->hasMethod($fieldName)) { + return $component->$fieldName(); + } + return $component->$fieldName; } } diff --git a/tests/model/VersionedTest.php b/tests/model/VersionedTest.php index 392037310..588272621 100644 --- a/tests/model/VersionedTest.php +++ b/tests/model/VersionedTest.php @@ -1,6 +1,11 @@ assertInstanceOf('Int', $obj2->dbObject('Version')); } + public function testVersionedFieldsNotInCMS() { + $obj = new VersionedTest_DataObject(); + + // the version field in cms causes issues with Versioned::augmentWrite() + $this->assertNull($obj->getCMSFields()->dataFieldByName('Version')); + } + public function testPublishCreateNewVersion() { $page1 = $this->objFromFixture('VersionedTest_DataObject', 'page1'); $page1->Content = 'orig'; @@ -442,6 +454,11 @@ class VersionedTest extends SapphireTest { } + +/** + * @package framework + * @subpackage tests + */ class VersionedTest_DataObject extends DataObject implements TestOnly { private static $db = array( "Name" => "Varchar", @@ -463,6 +480,10 @@ class VersionedTest_DataObject extends DataObject implements TestOnly { } +/** + * @package framework + * @subpackage tests + */ class VersionedTest_RelatedWithoutVersion extends DataObject implements TestOnly { private static $db = array( @@ -475,6 +496,10 @@ class VersionedTest_RelatedWithoutVersion extends DataObject implements TestOnly } +/** + * @package framework + * @subpackage tests + */ class VersionedTest_Subclass extends VersionedTest_DataObject implements TestOnly { private static $db = array( "ExtraField" => "Varchar", @@ -486,7 +511,8 @@ class VersionedTest_Subclass extends VersionedTest_DataObject implements TestOnl } /** - * @ignore + * @package framework + * @subpackage tests */ class VersionedTest_UnversionedWithField extends DataObject implements TestOnly { private static $db = array('Version' => 'Varchar(255)');