2007-07-19 12:40:28 +02:00
< ? php
/**
2011-04-15 11:35:30 +02:00
* The Versioned extension allows your DataObjects to have several versions , allowing
2007-07-19 12:40:28 +02:00
* you to rollback changes and view history . An example of this is the pages used in the CMS .
2012-04-12 08:02:46 +02:00
* @ package framework
2008-02-25 03:10:37 +01:00
* @ subpackage model
2007-07-19 12:40:28 +02:00
*/
2011-04-15 11:35:30 +02:00
class Versioned extends DataExtension {
2007-07-19 12:40:28 +02:00
/**
* An array of possible stages .
* @ var array
*/
protected $stages ;
/**
* The 'default' stage .
* @ var string
*/
protected $defaultStage ;
/**
* The 'live' stage .
* @ var string
*/
protected $liveStage ;
/**
* A version that a DataObject should be when it is 'migrating' ,
* that is , when it is in the process of moving from one stage to another .
* @ var string
*/
public $migratingVersion ;
2008-12-17 23:38:47 +01:00
/**
* A cache used by get_versionnumber_by_stage () .
* Clear through { @ link flushCache ()} .
*
* @ var array
*/
protected static $cache_versionnumber ;
2012-01-14 11:03:45 +01:00
/**
* @ var Boolean Flag which is temporarily changed during the write () process
* to influence augmentWrite () behaviour . If set to TRUE , no new version will be created
* for the following write . Needs to be public as other classes introspect this state
* during the write process in order to adapt to this versioning behaviour .
*/
public $_nextWriteWithoutVersion = false ;
2009-05-12 06:47:56 +02:00
/**
* Additional database columns for the new
* " _versions " table . Used in { @ link augmentDatabase ()}
2011-04-15 11:35:30 +02:00
* and all Versioned calls extending or creating
2009-05-12 06:47:56 +02:00
* SELECT statements .
*
* @ var array $db_for_versions_table
*/
static $db_for_versions_table = array (
" RecordID " => " Int " ,
" Version " => " Int " ,
" WasPublished " => " Boolean " ,
" AuthorID " => " Int " ,
2012-05-28 11:13:42 +02:00
" PublisherID " => " Int "
2009-05-12 06:47:56 +02:00
);
/**
* Additional database indexes for the new
* " _versions " table . Used in { @ link augmentDatabase ()} .
*
* @ var array $indexes_for_versions_table
*/
static $indexes_for_versions_table = array (
2012-09-11 02:53:08 +02:00
'RecordID_Version' => '("RecordID","Version")' ,
2009-05-12 06:47:56 +02:00
'RecordID' => true ,
'Version' => true ,
'AuthorID' => true ,
'PublisherID' => true ,
);
2009-08-11 06:45:54 +02:00
/**
* Reset static configuration variables to their default values
*/
2012-09-19 12:07:39 +02:00
public static function reset () {
2010-04-13 05:16:57 +02:00
self :: $reading_mode = '' ;
2009-08-11 06:45:54 +02:00
2010-04-13 05:16:57 +02:00
Session :: clear ( 'readingMode' );
2009-08-11 06:45:54 +02:00
}
2009-05-12 06:47:56 +02:00
2007-07-19 12:40:28 +02:00
/**
* 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
* considered the 'live' stage .
*/
2012-09-19 12:07:39 +02:00
public function __construct ( $stages = array ( 'Stage' , 'Live' )) {
2007-07-19 12:40:28 +02:00
parent :: __construct ();
if ( ! is_array ( $stages )) {
$stages = func_get_args ();
}
$this -> stages = $stages ;
$this -> defaultStage = reset ( $stages );
$this -> liveStage = array_pop ( $stages );
}
2011-12-22 03:30:46 +01:00
static $db = array (
'Version' => 'Int'
);
2012-09-19 12:07:39 +02:00
public static function get_extra_config ( $class , $extension , $args ) {
2012-08-22 23:29:13 +02:00
array (
'has_many' => array ( 'Versions' => $class )
);
2008-08-11 01:35:11 +02:00
}
2009-11-22 06:16:38 +01:00
/**
* Amend freshly created DataQuery objects with versioned - specific information
*/
2012-09-19 12:07:39 +02:00
public function augmentDataQueryCreation ( SQLQuery & $query , DataQuery & $dataQuery ) {
2011-03-21 07:53:09 +01:00
$parts = explode ( '.' , Versioned :: get_reading_mode ());
if ( $parts [ 0 ] == 'Archive' ) {
2009-11-22 06:16:38 +01:00
$dataQuery -> setQueryParam ( 'Versioned.mode' , 'archive' );
2011-03-21 07:53:09 +01:00
$dataQuery -> setQueryParam ( 'Versioned.date' , $parts [ 1 ]);
2009-11-22 06:16:38 +01:00
2011-03-21 07:53:09 +01:00
} else if ( $parts [ 0 ] == 'Stage' && $parts [ 1 ] != $this -> defaultStage && array_search ( $parts [ 1 ], $this -> stages ) !== false ) {
2009-11-22 06:16:38 +01:00
$dataQuery -> setQueryParam ( 'Versioned.mode' , 'stage' );
2011-03-21 07:53:09 +01:00
$dataQuery -> setQueryParam ( 'Versioned.stage' , $parts [ 1 ]);
2009-11-22 06:16:38 +01:00
}
}
/**
* Augment the the SQLQuery that is created by the DataQuery
* @ todo Should this all go into VersionedDataQuery ?
*/
2012-09-19 12:07:39 +02:00
public function augmentSQL ( SQLQuery & $query , DataQuery & $dataQuery = null ) {
2011-03-21 07:53:09 +01:00
$baseTable = ClassInfo :: baseDataClass ( $dataQuery -> dataClass ());
2009-11-22 06:16:38 +01:00
switch ( $dataQuery -> getQueryParam ( 'Versioned.mode' )) {
2011-03-21 07:53:09 +01:00
// Noop
case '' :
2012-05-28 11:13:42 +02:00
break ;
2009-11-22 06:16:38 +01:00
// Reading a specific data from the archive
case 'archive' :
$date = $dataQuery -> getQueryParam ( 'Versioned.date' );
2012-05-03 09:34:16 +02:00
foreach ( $query -> getFrom () as $table => $dummy ) {
2007-07-19 12:40:28 +02:00
$query -> renameTable ( $table , $table . '_versions' );
2012-08-10 03:54:29 +02:00
$query -> replaceText ( " \" { $table } _versions \" . \" ID \" " , " \" { $table } _versions \" . \" RecordID \" " );
$query -> replaceText ( " ` { $table } _versions`.`ID` " , " ` { $table } _versions`.`RecordID` " );
2009-05-12 06:47:56 +02:00
// Add all <basetable>_versions columns
foreach ( self :: $db_for_versions_table as $name => $type ) {
2012-05-01 06:42:14 +02:00
$query -> selectField ( sprintf ( '"%s_versions"."%s"' , $baseTable , $name ), $name );
2009-05-12 06:47:56 +02:00
}
2012-05-01 06:42:14 +02:00
$query -> selectField ( sprintf ( '"%s_versions"."%s"' , $baseTable , 'RecordID' ), " ID " );
2007-07-19 12:40:28 +02:00
if ( $table != $baseTable ) {
2012-08-10 02:45:37 +02:00
$query -> addWhere ( " \" { $table } _versions \" . \" Version \" = \" { $baseTable } _versions \" . \" Version \" " );
2007-07-19 12:40:28 +02:00
}
}
// Link to the version archived on that date
2012-08-20 03:24:28 +02:00
$safeDate = Convert :: raw2sql ( $date );
$query -> addWhere (
2012-08-21 03:46:19 +02:00
" \" { $baseTable } _versions \" . \" Version \" IN
2012-08-20 03:24:28 +02:00
( SELECT LatestVersion FROM
( SELECT
2012-08-21 03:46:19 +02:00
\ " { $baseTable } _versions \" . \" RecordID \" ,
MAX ( \ " { $baseTable } _versions \" . \" Version \" ) AS LatestVersion
FROM \ " { $baseTable } _versions \"
WHERE \ " { $baseTable } _versions \" . \" LastEdited \" <= ' $safeDate '
GROUP BY \ " { $baseTable } _versions \" . \" RecordID \"
) AS \ " { $baseTable } _versions_latest \"
WHERE \ " { $baseTable } _versions_latest \" . \" RecordID \" = \" { $baseTable } _versions \" . \" RecordID \"
2012-08-20 03:24:28 +02:00
) " );
2009-11-22 06:16:38 +01:00
break ;
// Reading a specific stage (Stage or Live)
case 'stage' :
$stage = $dataQuery -> getQueryParam ( 'Versioned.stage' );
if ( $stage && ( $stage != $this -> defaultStage )) {
2012-05-03 09:34:16 +02:00
foreach ( $query -> getFrom () as $table => $dummy ) {
2009-11-22 06:16:38 +01:00
// Only rewrite table names that are actually part of the subclass tree
// This helps prevent rewriting of other tables that get joined in, in
// particular, many_many tables
if ( class_exists ( $table ) && ( $table == $this -> owner -> class
|| is_subclass_of ( $table , $this -> owner -> class )
|| is_subclass_of ( $this -> owner -> class , $table ))) {
$query -> renameTable ( $table , $table . '_' . $stage );
}
}
2007-07-19 12:40:28 +02:00
}
2009-11-22 06:16:38 +01:00
break ;
2012-08-21 05:52:12 +02:00
// Reading a specific stage, but only return items that aren't in any other stage
case 'stage_unique' :
$stage = $dataQuery -> getQueryParam ( 'Versioned.stage' );
// Recurse to do the default stage behavior (must be first, we rely on stage renaming happening before below)
$dataQuery -> setQueryParam ( 'Versioned.mode' , 'stage' );
$this -> augmentSQL ( $query , $dataQuery );
// Now exclude any ID from any other stage. Note that we double rename to avoid the regular stage rename
// renaming all subquery references to be Versioned.stage
foreach ( $this -> stages as $excluding ) {
if ( $excluding == $stage ) continue ;
$tempName = 'ExclusionarySource_' . $excluding ;
$excludingTable = $baseTable . ( $excluding && $excluding != $this -> defaultStage ? " _ $excluding " : '' );
2012-08-23 02:56:06 +02:00
$query -> addWhere ( '"' . $baseTable . '"."ID" NOT IN (SELECT "ID" FROM "' . $tempName . '")' );
2012-08-21 05:52:12 +02:00
$query -> renameTable ( $tempName , $excludingTable );
}
break ;
2011-03-21 07:53:09 +01:00
// Return all version instances
case 'all_versions' :
case 'latest_versions' :
2012-05-28 11:13:42 +02:00
foreach ( $query -> getFrom () as $alias => $join ) {
if ( $alias != $baseTable ) {
$query -> setJoinFilter ( $alias , " \" $alias\ " . \ " RecordID \" = \" { $baseTable } _versions \" . \" RecordID \" AND \" $alias\ " . \ " Version \" = \" { $baseTable } _versions \" . \" Version \" " );
}
$query -> renameTable ( $alias , $alias . '_versions' );
}
2011-03-21 07:53:09 +01:00
2012-05-28 11:13:42 +02:00
// Add all <basetable>_versions columns
foreach ( self :: $db_for_versions_table as $name => $type ) {
$query -> selectField ( sprintf ( '"%s_versions"."%s"' , $baseTable , $name ), $name );
}
$query -> selectField ( sprintf ( '"%s_versions"."%s"' , $baseTable , 'RecordID' ), " ID " );
2012-08-21 06:23:33 +02:00
$query -> addOrderBy ( sprintf ( '"%s_versions"."%s"' , $baseTable , 'Version' ));
2012-05-28 11:13:42 +02:00
// latest_version has one more step
// Return latest version instances, regardless of whether they are on a particular stage
// This provides "show all, including deleted" functonality
if ( $dataQuery -> getQueryParam ( 'Versioned.mode' ) == 'latest_versions' ) {
2012-08-20 03:24:28 +02:00
$query -> addWhere (
2012-08-21 03:46:19 +02:00
" \" { $alias } _versions \" . \" Version \" IN
2012-08-20 03:24:28 +02:00
( SELECT LatestVersion FROM
( SELECT
2012-08-21 03:46:19 +02:00
\ " { $alias } _versions \" . \" RecordID \" ,
MAX ( \ " { $alias } _versions \" . \" Version \" ) AS LatestVersion
FROM \ " { $alias } _versions \"
GROUP BY \ " { $alias } _versions \" . \" RecordID \"
) AS \ " { $alias } _versions_latest \"
WHERE \ " { $alias } _versions_latest \" . \" RecordID \" = \" { $alias } _versions \" . \" RecordID \"
2012-08-20 03:24:28 +02:00
) " );
2012-05-28 11:13:42 +02:00
}
break ;
default :
throw new InvalidArgumentException ( " Bad value for query parameter Versioned.mode: " . $dataQuery -> getQueryParam ( 'Versioned.mode' ));
2007-07-19 12:40:28 +02:00
}
}
2009-05-21 07:08:11 +02:00
/**
* Keep track of the archive tables that have been created
*/
private static $archive_tables = array ();
2009-05-21 10:08:01 +02:00
/**
* Called by { @ link SapphireTest } when the database is reset .
* @ todo Reduce the coupling between this and SapphireTest , somehow .
*/
public static function on_db_reset () {
2010-04-12 05:21:30 +02:00
// Drop all temporary tables
$db = DB :: getConn ();
foreach ( self :: $archive_tables as $tableName ) {
if ( method_exists ( $db , 'dropTable' )) $db -> dropTable ( $tableName );
else $db -> query ( " DROP TABLE \" $tableName\ " " );
}
// Remove references to them
2009-05-21 10:08:01 +02:00
self :: $archive_tables = array ();
}
2009-05-01 05:49:34 +02:00
2007-09-16 18:15:47 +02:00
/**
* An array of DataObject extensions that may require versioning for extra tables
2007-09-16 19:24:51 +02:00
* 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'
2007-09-16 18:15:47 +02:00
* and Extension2 the tables 'Class_suffix2' and 'Class_suffix3' :
*
* $versionableExtensions = array (
* 'Extension1' => 'suffix1' ,
* 'Extension2' => array ( 'suffix2' , 'suffix3' ),
* );
2007-09-16 19:24:51 +02:00
*
* Make sure your extension has a static $enabled - property that determines if it is
* processed by Versioned .
2007-09-16 18:15:47 +02:00
*
* @ var array
*/
protected static $versionableExtensions = array ( 'Translatable' => 'lang' );
2007-07-19 12:40:28 +02:00
2012-09-19 12:07:39 +02:00
public function augmentDatabase () {
2007-09-16 18:15:47 +02:00
$classTable = $this -> owner -> class ;
2009-07-09 08:00:07 +02:00
$isRootClass = ( $this -> owner -> class == ClassInfo :: baseDataClass ( $this -> owner -> class ));
2007-09-16 18:15:47 +02:00
// Build a list of suffixes whose tables need versioning
$allSuffixes = array ();
foreach ( Versioned :: $versionableExtensions as $versionableExtension => $suffixes ) {
2009-08-11 10:49:52 +02:00
if ( $this -> owner -> hasExtension ( $versionableExtension )) {
2007-09-16 18:15:47 +02:00
$allSuffixes = array_merge ( $allSuffixes , ( array ) $suffixes );
foreach (( array ) $suffixes as $suffix ) {
$allSuffixes [ $suffix ] = $versionableExtension ;
}
}
}
2007-07-19 12:40:28 +02:00
2007-09-16 19:24:51 +02:00
// Add the default table with an empty suffix to the list (table name = class name)
2007-09-16 18:15:47 +02:00
array_push ( $allSuffixes , '' );
foreach ( $allSuffixes as $key => $suffix ) {
// check that this is a valid suffix
if ( ! is_int ( $key )) continue ;
if ( $suffix ) $table = " { $classTable } _ $suffix " ;
else $table = $classTable ;
2009-08-11 10:49:52 +02:00
if ( $fields = DataObject :: database_fields ( $this -> owner -> class )) {
2012-04-18 13:10:57 +02:00
$options = Config :: inst () -> get ( $this -> owner -> class , 'create_table_options' , Config :: FIRST_SET );
2007-09-16 19:24:51 +02:00
$indexes = $this -> owner -> databaseIndexes ();
2010-05-25 05:55:30 +02:00
if ( $suffix && ( $ext = $this -> owner -> getExtensionInstance ( $allSuffixes [ $suffix ]))) {
2007-09-16 18:58:19 +02:00
if ( ! $ext -> isVersionedTable ( $table )) continue ;
2009-06-04 08:48:44 +02:00
$ext -> setOwner ( $this -> owner );
2007-09-16 18:58:19 +02:00
$fields = $ext -> fieldsInExtraTables ( $suffix );
2009-06-04 08:48:44 +02:00
$ext -> clearOwner ();
2007-09-16 18:15:47 +02:00
$indexes = $fields [ 'indexes' ];
$fields = $fields [ 'db' ];
2007-09-16 19:24:51 +02:00
}
2007-07-19 12:40:28 +02:00
2007-09-16 19:24:51 +02:00
// Create tables for other stages
foreach ( $this -> stages as $stage ) {
// Extra tables for _Live, etc.
2010-11-24 07:23:49 +01:00
//Change unique indexes to 'index'. Versioned tables may run into unique indexing difficulties otherwise.
foreach ( $indexes as $key => $index ){
if ( is_array ( $index ) && $index [ 'type' ] == 'unique' ){
$indexes [ $key ][ 'type' ] = 'index' ;
}
}
2007-09-16 19:24:51 +02:00
if ( $stage != $this -> defaultStage ) {
2011-09-16 14:21:54 +02:00
DB :: requireTable ( " { $table } _ $stage " , $fields , $indexes , false , $options );
2007-09-16 19:24:51 +02:00
}
// Version fields on each root table (including Stage)
2009-05-07 08:00:50 +02:00
/*
2009-07-09 08:00:07 +02:00
if ( $isRootClass ) {
2007-09-16 19:24:51 +02:00
$stageTable = ( $stage == $this -> defaultStage ) ? $table : " { $table } _ $stage " ;
2008-11-23 02:01:03 +01:00
$parts = Array ( 'datatype' => 'int' , 'precision' => 11 , 'null' => 'not null' , 'default' => ( int ) 0 );
$values = Array ( 'type' => 'int' , 'parts' => $parts );
DB :: requireField ( $stageTable , 'Version' , $values );
2007-07-19 12:40:28 +02:00
}
2009-05-07 08:00:50 +02:00
*/
2007-07-19 12:40:28 +02:00
}
2007-09-16 19:24:51 +02:00
2009-07-09 08:00:07 +02:00
if ( $isRootClass ) {
// Create table for all versions
$versionFields = array_merge (
self :: $db_for_versions_table ,
( array ) $fields
);
2007-09-16 19:24:51 +02:00
2009-07-09 08:00:07 +02:00
$versionIndexes = array_merge (
self :: $indexes_for_versions_table ,
( array ) $indexes
);
} else {
// Create fields for any tables of subclasses
$versionFields = array_merge (
array (
" RecordID " => " Int " ,
" Version " => " Int " ,
),
( array ) $fields
);
2007-09-16 19:24:51 +02:00
2010-11-01 03:58:43 +01:00
//Unique indexes will not work on versioned tables, so we'll convert them to standard indexes:
foreach ( $indexes as $key => $index ){
if ( is_array ( $index ) && strtolower ( $index [ 'type' ]) == 'unique' ){
$indexes [ $key ][ 'type' ] = 'index' ;
}
}
2009-07-09 08:00:07 +02:00
$versionIndexes = array_merge (
array (
2012-08-21 06:24:05 +02:00
'RecordID_Version' => array ( 'type' => 'unique' , 'value' => '"RecordID","Version"' ),
2009-07-09 08:00:07 +02:00
'RecordID' => true ,
'Version' => true ,
),
( array ) $indexes
);
2007-09-16 19:24:51 +02:00
}
2010-10-15 04:55:15 +02:00
if ( DB :: getConn () -> hasTable ( " { $table } _versions " )) {
2010-10-19 05:01:51 +02:00
// Fix data that lacks the uniqueness constraint (since this was added later and
// bugs meant that the constraint was validated)
2010-10-15 04:55:15 +02:00
$duplications = DB :: query ( " SELECT MIN( \" ID \" ) AS \" ID \" , \" RecordID \" , \" Version \"
FROM \ " { $table } _versions \" GROUP BY \" RecordID \" , \" Version \"
HAVING COUNT ( * ) > 1 " );
foreach ( $duplications as $dup ) {
DB :: alteration_message ( " Removing { $table } _versions duplicate data for "
. " { $dup [ 'RecordID' ] } / { $dup [ 'Version' ] } " , " deleted " );
DB :: query ( " DELETE FROM \" { $table } _versions \" WHERE \" RecordID \" = { $dup [ 'RecordID' ] }
AND \ " Version \" = { $dup [ 'Version' ] } AND \" ID \" != { $dup [ 'ID' ] } " );
}
2010-10-19 05:01:51 +02:00
// Remove junk which has no data in parent classes. Only needs to run the following
// when versioned data is spread over multiple tables
if ( ! $isRootClass && ( $versionedTables = ClassInfo :: dataClassesFor ( $table ))) {
foreach ( $versionedTables as $child ) {
if ( $table == $child ) break ; // only need subclasses
$count = DB :: query ( "
SELECT COUNT ( * ) FROM \ " { $table } _versions \"
LEFT JOIN \ " { $child } _versions \"
2010-10-19 05:03:19 +02:00
ON \ " { $child } _versions \" . \" RecordID \" = \" { $table } _versions \" . \" RecordID \"
AND \ " { $child } _versions \" . \" Version \" = \" { $table } _versions \" . \" Version \"
WHERE \ " { $child } _versions \" . \" ID \" IS NULL
2010-10-19 05:01:51 +02:00
" )->value();
if ( $count > 0 ) {
DB :: alteration_message ( " Removing orphaned versioned records " , " deleted " );
$effectedIDs = DB :: query ( "
2010-10-19 05:03:19 +02:00
SELECT \ " { $table } _versions \" . \" ID \" FROM \" { $table } _versions \"
2010-10-19 05:01:51 +02:00
LEFT JOIN \ " { $child } _versions \"
2010-10-19 05:03:19 +02:00
ON \ " { $child } _versions \" . \" RecordID \" = \" { $table } _versions \" . \" RecordID \"
AND \ " { $child } _versions \" . \" Version \" = \" { $table } _versions \" . \" Version \"
WHERE \ " { $child } _versions \" . \" ID \" IS NULL
2010-10-19 05:01:51 +02:00
" )->column();
if ( is_array ( $effectedIDs )) {
foreach ( $effectedIDs as $key => $value ) {
2010-10-19 05:06:15 +02:00
DB :: query ( " DELETE FROM \" { $table } _versions \" WHERE \" { $table } _versions \" . \" ID \" = ' $value ' " );
2010-10-19 05:01:51 +02:00
}
}
}
}
}
2010-10-15 04:55:15 +02:00
}
2009-07-09 08:00:07 +02:00
2011-09-16 14:21:54 +02:00
DB :: requireTable ( " { $table } _versions " , $versionFields , $versionIndexes , true , $options );
2007-09-16 19:24:51 +02:00
} else {
DB :: dontRequireTable ( " { $table } _versions " );
foreach ( $this -> stages as $stage ) {
if ( $stage != $this -> defaultStage ) DB :: dontrequireTable ( " { $table } _ $stage " );
2007-07-19 12:40:28 +02:00
}
2007-09-16 18:15:47 +02:00
}
2007-07-19 12:40:28 +02:00
}
}
/**
* Augment a write - record request .
* @ param SQLQuery $manipulation Query to augment .
*/
2012-09-19 12:07:39 +02:00
public function augmentWrite ( & $manipulation ) {
2007-07-19 12:40:28 +02:00
$tables = array_keys ( $manipulation );
2007-09-16 18:15:47 +02:00
$version_table = array ();
2007-07-19 12:40:28 +02:00
foreach ( $tables as $table ) {
2009-07-09 08:00:07 +02:00
$baseDataClass = ClassInfo :: baseDataClass ( $table );
2009-08-24 09:35:05 +02:00
$isRootClass = ( $table == $baseDataClass );
2007-07-19 12:40:28 +02:00
// Make sure that the augmented write is being applied to a table that can be versioned
2007-09-16 18:15:47 +02:00
if ( ! $this -> canBeVersioned ( $table ) ) {
2007-07-19 12:40:28 +02:00
unset ( $manipulation [ $table ]);
continue ;
}
2009-07-09 08:00:07 +02:00
$id = $manipulation [ $table ][ 'id' ] ? $manipulation [ $table ][ 'id' ] : $manipulation [ $table ][ 'fields' ][ 'ID' ];;
2007-09-16 18:15:47 +02:00
if ( ! $id ) user_error ( " Couldn't find ID in " . var_export ( $manipulation [ $table ], true ), E_USER_ERROR );
2007-07-19 12:40:28 +02:00
2007-09-16 18:15:47 +02:00
$rid = isset ( $manipulation [ $table ][ 'RecordID' ]) ? $manipulation [ $table ][ 'RecordID' ] : $id ;
2007-07-19 12:40:28 +02:00
$newManipulation = array (
" command " => " insert " ,
" fields " => isset ( $manipulation [ $table ][ 'fields' ]) ? $manipulation [ $table ][ 'fields' ] : null
);
if ( $this -> migratingVersion ) {
$manipulation [ $table ][ 'fields' ][ 'Version' ] = $this -> migratingVersion ;
}
2010-10-12 23:48:07 +02:00
2009-07-09 08:00:07 +02:00
// If we haven't got a version #, then we're creating a new version.
// Otherwise, we're just copying a version to another table
2007-07-19 12:40:28 +02:00
if ( ! isset ( $manipulation [ $table ][ 'fields' ][ 'Version' ])) {
// Add any extra, unchanged fields to the version record.
2008-11-24 00:28:16 +01:00
$data = DB :: query ( " SELECT * FROM \" $table\ " WHERE \ " ID \" = $id " ) -> record ();
2007-07-19 12:40:28 +02:00
if ( $data ) foreach ( $data as $k => $v ) {
2011-09-15 14:15:41 +02:00
if ( ! isset ( $newManipulation [ 'fields' ][ $k ])) $newManipulation [ 'fields' ][ $k ] = " ' " . Convert :: raw2sql ( $v ) . " ' " ;
2007-07-19 12:40:28 +02:00
}
// Set up a new entry in (table)_versions
2007-09-16 18:15:47 +02:00
$newManipulation [ 'fields' ][ 'RecordID' ] = $rid ;
2007-07-19 12:40:28 +02:00
unset ( $newManipulation [ 'fields' ][ 'ID' ]);
// Create a new version #
2007-09-16 18:15:47 +02:00
if ( isset ( $version_table [ $table ])) $nextVersion = $version_table [ $table ];
else unset ( $nextVersion );
2009-07-09 08:00:07 +02:00
if ( $rid && ! isset ( $nextVersion )) $nextVersion = DB :: query ( " SELECT MAX( \" Version \" ) + 1 FROM \" { $baseDataClass } _versions \" WHERE \" RecordID \" = $rid " ) -> value ();
2007-07-19 12:40:28 +02:00
$newManipulation [ 'fields' ][ 'Version' ] = $nextVersion ? $nextVersion : 1 ;
2009-07-09 08:00:07 +02:00
if ( $isRootClass ) {
$userID = ( Member :: currentUser ()) ? Member :: currentUser () -> ID : 0 ;
$newManipulation [ 'fields' ][ 'AuthorID' ] = $userID ;
}
2007-07-19 12:40:28 +02:00
$manipulation [ " { $table } _versions " ] = $newManipulation ;
// Add the version number to this data
$manipulation [ $table ][ 'fields' ][ 'Version' ] = $newManipulation [ 'fields' ][ 'Version' ];
2007-09-16 18:15:47 +02:00
$version_table [ $table ] = $nextVersion ;
2007-07-19 12:40:28 +02:00
}
// Putting a Version of -1 is a signal to leave the version table alone, despite their being no version
2012-01-14 11:03:45 +01:00
if ( $manipulation [ $table ][ 'fields' ][ 'Version' ] < 0 || $this -> _nextWriteWithoutVersion ) {
unset ( $manipulation [ $table ][ 'fields' ][ 'Version' ]);
}
2007-07-19 12:40:28 +02:00
2007-09-16 18:58:19 +02:00
if ( ! $this -> hasVersionField ( $table )) unset ( $manipulation [ $table ][ 'fields' ][ 'Version' ]);
2007-07-19 12:40:28 +02:00
// Grab a version number - it should be the same across all tables.
if ( isset ( $manipulation [ $table ][ 'fields' ][ 'Version' ])) $thisVersion = $manipulation [ $table ][ 'fields' ][ 'Version' ];
// If we're editing Live, then use (table)_Live instead of (table)
2010-04-13 05:16:57 +02:00
if ( Versioned :: current_stage () && Versioned :: current_stage () != $this -> defaultStage ) {
2010-10-15 05:53:14 +02:00
// If the record has already been inserted in the (table), get rid of it.
if ( $manipulation [ $table ][ 'command' ] == 'insert' ) {
DB :: query ( " DELETE FROM \" { $table } \" WHERE \" ID \" =' $id ' " );
}
2010-04-13 05:16:57 +02:00
$newTable = $table . '_' . Versioned :: current_stage ();
2007-07-19 12:40:28 +02:00
$manipulation [ $newTable ] = $manipulation [ $table ];
unset ( $manipulation [ $table ]);
}
}
2010-10-15 04:55:15 +02:00
// Clear the migration flag
if ( $this -> migratingVersion ) $this -> migrateVersion ( null );
2010-10-13 03:04:38 +02:00
2007-07-19 12:40:28 +02:00
// Add the new version # back into the data object, for accessing after this write
2007-09-16 18:15:47 +02:00
if ( isset ( $thisVersion )) $this -> owner -> Version = str_replace ( " ' " , " " , $thisVersion );
}
2010-10-12 23:48:07 +02:00
2012-01-14 11:03:45 +01:00
/**
* Perform a write without affecting the version table .
* On objects without versioning .
*
* @ return int The ID of the record
*/
public function writeWithoutVersion () {
$this -> _nextWriteWithoutVersion = true ;
return $this -> owner -> write ();
}
2012-09-19 12:07:39 +02:00
public function onAfterWrite () {
2012-01-14 11:03:45 +01:00
$this -> _nextWriteWithoutVersion = false ;
}
2010-10-12 23:48:07 +02:00
/**
* If a write was skipped , then we need to ensure that we don ' t leave a migrateVersion ()
* value lying around for the next write .
*/
2012-09-19 12:07:39 +02:00
public function onAfterSkippedWrite () {
2010-10-12 23:48:07 +02:00
$this -> migrateVersion ( null );
}
2007-09-16 18:15:47 +02:00
2007-09-16 18:58:19 +02:00
/**
* Determine if a table is supporting the Versioned extensions ( e . g . $table_versions does exists )
*
* @ param string $table Table name
* @ return boolean
*/
2012-09-19 12:07:39 +02:00
public function canBeVersioned ( $table ) {
2009-08-11 10:49:52 +02:00
return ClassInfo :: exists ( $table )
2011-02-21 06:20:14 +01:00
&& is_subclass_of ( $table , 'DataObject' )
2009-08-11 10:49:52 +02:00
&& DataObject :: has_own_table ( $table );
2007-09-16 18:15:47 +02:00
}
2007-09-16 18:58:19 +02:00
/**
* 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
*/
2012-09-19 12:07:39 +02:00
public function hasVersionField ( $table ) {
2010-10-12 23:42:45 +02:00
$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 ));
2007-09-16 18:58:19 +02:00
}
2012-09-19 12:07:39 +02:00
public function extendWithSuffix ( $table ) {
2007-09-16 18:15:47 +02:00
foreach ( Versioned :: $versionableExtensions as $versionableExtension => $suffixes ) {
if ( $this -> owner -> hasExtension ( $versionableExtension )) {
2010-05-25 05:55:30 +02:00
$ext = $this -> owner -> getExtensionInstance ( $versionableExtension );
2009-06-04 08:48:44 +02:00
$ext -> setOwner ( $this -> owner );
$table = $ext -> extendWithSuffix ( $table );
$ext -> clearOwner ();
2007-09-16 18:15:47 +02:00
}
}
return $table ;
2007-07-19 12:40:28 +02:00
}
//-----------------------------------------------------------------------------------------------//
/**
* Get the latest published DataObject .
* @ return DataObject
*/
2012-09-19 12:07:39 +02:00
public function latestPublished () {
2007-07-19 12:40:28 +02:00
// Get the root data object class - this will have the version field
$table1 = $this -> owner -> class ;
while ( ( $p = get_parent_class ( $table1 )) != " DataObject " ) $table1 = $p ;
$table2 = $table1 . " _ $this->liveStage " ;
2008-11-24 00:28:16 +01:00
return DB :: query ( " SELECT \" $table1\ " . \ " Version \" = \" $table2\ " . \ " Version \" FROM \" $table1\ " INNER JOIN \ " $table2\ " ON \ " $table1\ " . \ " ID \" = \" $table2\ " . \ " ID \" WHERE \" $table1\ " . \ " ID \" = " . $this -> owner -> ID ) -> value ();
2007-07-19 12:40:28 +02:00
}
/**
* 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 number will be copied over .
*/
2012-09-19 12:07:39 +02:00
public function publish ( $fromStage , $toStage , $createNewVersion = false ) {
2010-12-11 02:28:46 +01:00
$this -> owner -> extend ( 'onBeforeVersionedPublish' , $fromStage , $toStage , $createNewVersion );
2007-07-19 12:40:28 +02:00
$baseClass = $this -> owner -> class ;
while ( ( $p = get_parent_class ( $baseClass )) != " DataObject " ) $baseClass = $p ;
2009-08-27 06:43:40 +02:00
$extTable = $this -> extendWithSuffix ( $baseClass );
2007-07-19 12:40:28 +02:00
if ( is_numeric ( $fromStage )) {
2009-07-03 03:21:13 +02:00
$from = Versioned :: get_version ( $baseClass , $this -> owner -> ID , $fromStage );
2007-07-19 12:40:28 +02:00
} else {
$this -> owner -> flushCache ();
2009-07-03 03:21:13 +02:00
$from = Versioned :: get_one_by_stage ( $baseClass , $fromStage , " \" { $baseClass } \" . \" ID \" = { $this -> owner -> ID } " );
2007-07-19 12:40:28 +02:00
}
2007-07-20 01:15:05 +02:00
$publisherID = isset ( Member :: currentUser () -> ID ) ? Member :: currentUser () -> ID : 0 ;
2007-07-19 12:40:28 +02:00
if ( $from ) {
$from -> forceChange ();
2010-10-13 03:04:38 +02:00
if ( $createNewVersion ) {
$latest = self :: get_latest_version ( $baseClass , $this -> owner -> ID );
$this -> owner -> Version = $latest -> Version + 1 ;
} else {
$from -> migrateVersion ( $from -> Version );
}
2007-07-19 12:40:28 +02:00
// Mark this version as having been published at some stage
2008-11-24 10:31:14 +01:00
DB :: query ( " UPDATE \" { $extTable } _versions \" SET \" WasPublished \" = '1', \" PublisherID \" = $publisherID WHERE \" RecordID \" = $from->ID AND \" Version \" = $from->Version " );
2007-07-19 12:40:28 +02:00
2010-04-13 05:16:57 +02:00
$oldMode = Versioned :: get_reading_mode ();
Versioned :: reading_stage ( $toStage );
2009-07-02 00:27:18 +02:00
$conn = DB :: getConn ();
2009-08-08 06:23:05 +02:00
if ( method_exists ( $conn , 'allowPrimaryKeyEditing' )) $conn -> allowPrimaryKeyEditing ( $baseClass , true );
2007-07-19 12:40:28 +02:00
$from -> write ();
2009-08-08 06:23:05 +02:00
if ( method_exists ( $conn , 'allowPrimaryKeyEditing' )) $conn -> allowPrimaryKeyEditing ( $baseClass , false );
2009-07-02 00:27:18 +02:00
2007-07-19 12:40:28 +02:00
$from -> destroy ();
2010-04-13 05:16:57 +02:00
Versioned :: set_reading_mode ( $oldMode );
2007-07-19 12:40:28 +02:00
} else {
user_error ( " Can't find { $this -> owner -> URLSegment } / { $this -> owner -> ID } in stage $fromStage " , E_USER_WARNING );
}
}
/**
* Set the migrating version .
* @ param string $version The version .
*/
2012-09-19 12:07:39 +02:00
public function migrateVersion ( $version ) {
2007-07-19 12:40:28 +02:00
$this -> migratingVersion = $version ;
}
/**
* 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
*/
2012-09-19 12:07:39 +02:00
public function stagesDiffer ( $stage1 , $stage2 ) {
2007-07-19 12:40:28 +02:00
$table1 = $this -> baseTable ( $stage1 );
$table2 = $this -> baseTable ( $stage2 );
if ( ! is_numeric ( $this -> owner -> ID )) {
return true ;
}
2012-05-28 11:13:42 +02:00
2007-07-19 12:40:28 +02:00
// We test for equality - if one of the versions doesn't exist, this will be false
2008-11-23 02:01:03 +01:00
//TODO: DB Abstraction: if statement here:
2008-11-24 00:28:16 +01:00
$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 ();
2007-07-19 12:40:28 +02:00
return ! $stagesAreEqual ;
}
2012-09-19 12:07:39 +02:00
public function Versions ( $filter = " " , $sort = " " , $limit = " " , $join = " " , $having = " " ) {
2009-09-14 07:15:49 +02:00
return $this -> allVersions ( $filter , $sort , $limit , $join , $having );
2008-08-11 01:35:11 +02:00
}
2007-07-19 12:40:28 +02:00
/**
* Return a list of all the versions available .
* @ param string $filter
*/
2011-09-27 04:14:01 +02:00
public function allVersions ( $filter = " " , $sort = " " , $limit = " " , $join = " " , $having = " " ) {
2010-10-13 02:50:48 +02:00
// Make sure the table names are not postfixed (e.g. _Live)
$oldMode = self :: get_reading_mode ();
self :: reading_stage ( 'Stage' );
2011-10-29 06:12:37 +02:00
2012-07-24 04:17:12 +02:00
$list = DataObject :: get ( get_class ( $this -> owner ), $filter , $sort , $join , $limit );
2011-10-29 06:12:37 +02:00
if ( $having ) $having = $list -> having ( $having );
$query = $list -> dataQuery () -> query ();
2007-07-19 12:40:28 +02:00
2012-05-03 09:34:16 +02:00
foreach ( $query -> getFrom () as $table => $tableJoin ) {
2011-09-27 04:14:01 +02:00
if ( is_string ( $tableJoin ) && $tableJoin [ 0 ] == '"' ) {
$baseTable = str_replace ( '"' , '' , $tableJoin );
} elseif ( is_string ( $tableJoin ) && substr ( $tableJoin , 0 , 5 ) != 'INNER' ) {
2012-05-03 09:34:16 +02:00
$query -> setFrom ( array ( $table => " LEFT JOIN \" $table\ " ON \ " $table\ " . \ " RecordID \" = \" { $baseTable } _versions \" . \" RecordID \" AND \" $table\ " . \ " Version \" = \" { $baseTable } _versions \" . \" Version \" " ));
2011-09-27 04:14:01 +02:00
}
2007-07-19 12:40:28 +02:00
$query -> renameTable ( $table , $table . '_versions' );
}
2009-05-12 06:47:56 +02:00
// Add all <basetable>_versions columns
foreach ( self :: $db_for_versions_table as $name => $type ) {
2012-05-01 06:42:14 +02:00
$query -> selectField ( sprintf ( '"%s_versions"."%s"' , $baseTable , $name ), $name );
2009-05-12 06:47:56 +02:00
}
2007-07-19 12:40:28 +02:00
2012-05-03 09:34:16 +02:00
$query -> addWhere ( " \" { $baseTable } _versions \" . \" RecordID \" = ' { $this -> owner -> ID } ' " );
$query -> setOrderBy (( $sort ) ? $sort : " \" { $baseTable } _versions \" . \" LastEdited \" DESC, \" { $baseTable } _versions \" . \" Version \" DESC " );
2007-07-19 12:40:28 +02:00
$records = $query -> execute ();
2011-05-05 12:40:24 +02:00
$versions = new ArrayList ();
2012-05-03 09:34:16 +02:00
2007-07-19 12:40:28 +02:00
foreach ( $records as $record ) {
$versions -> push ( new Versioned_Version ( $record ));
}
2010-10-13 02:50:48 +02:00
Versioned :: set_reading_mode ( $oldMode );
2007-07-19 12:40:28 +02:00
return $versions ;
}
/**
* 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
*/
2012-09-19 12:07:39 +02:00
public function compareVersions ( $from , $to ) {
2007-07-19 12:40:28 +02:00
$fromRecord = Versioned :: get_version ( $this -> owner -> class , $this -> owner -> ID , $from );
$toRecord = Versioned :: get_version ( $this -> owner -> class , $this -> owner -> ID , $to );
2009-05-23 05:29:33 +02:00
$diff = new DataDifferencer ( $fromRecord , $toRecord );
return $diff -> diffedData ();
2007-07-19 12:40:28 +02:00
}
/**
* Return the base table - the class that directly extends DataObject .
* @ return string
*/
2012-09-19 12:07:39 +02:00
public function baseTable ( $stage = null ) {
2007-07-19 12:40:28 +02:00
$tableClasses = ClassInfo :: dataClassesFor ( $this -> owner -> class );
$baseClass = array_shift ( $tableClasses );
return ( ! $stage || $stage == $this -> defaultStage ) ? $baseClass : $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' .
*/
2012-09-19 12:07:39 +02:00
public static function choose_site_stage () {
2007-07-19 12:40:28 +02:00
if ( isset ( $_GET [ 'stage' ])) {
2010-10-19 03:32:39 +02:00
$stage = ucfirst ( strtolower ( $_GET [ 'stage' ]));
if ( ! in_array ( $stage , array ( 'Stage' , 'Live' ))) $stage = 'Live' ;
Session :: set ( 'readingMode' , 'Stage.' . $stage );
2007-07-19 12:40:28 +02:00
}
2007-11-09 04:40:05 +01:00
if ( isset ( $_GET [ 'archiveDate' ])) {
2010-04-13 05:16:57 +02:00
Session :: set ( 'readingMode' , 'Archive.' . $_GET [ 'archiveDate' ]);
2007-11-09 04:40:05 +01:00
}
2010-10-19 03:32:39 +02:00
if ( $mode = Session :: get ( 'readingMode' )) {
Versioned :: set_reading_mode ( $mode );
2007-07-19 12:40:28 +02:00
} else {
Versioned :: reading_stage ( " Live " );
}
2010-04-12 04:33:46 +02:00
2011-08-30 08:38:03 +02:00
if ( ! headers_sent () && ! Director :: is_cli ()) {
2010-10-15 03:22:25 +02:00
if ( Versioned :: current_stage () == 'Live' ) {
2012-04-26 06:43:58 +02:00
// clear the cookie if it's set
if ( ! empty ( $_COOKIE [ 'bypassStaticCache' ])) {
Cookie :: set ( 'bypassStaticCache' , null , 0 , null , null , false , true /* httponly */ );
unset ( $_COOKIE [ 'bypassStaticCache' ]);
}
2010-10-15 03:22:25 +02:00
} else {
2012-04-26 06:43:58 +02:00
// set the cookie if it's cleared
if ( empty ( $_COOKIE [ 'bypassStaticCache' ])) {
Cookie :: set ( 'bypassStaticCache' , '1' , 0 , null , null , false , true /* httponly */ );
$_COOKIE [ 'bypassStaticCache' ] = 1 ;
}
2010-10-15 03:22:25 +02:00
}
2010-04-12 04:33:46 +02:00
}
2007-07-19 12:40:28 +02:00
}
2010-04-13 05:16:57 +02:00
/**
* Set the current reading mode .
*/
2012-09-19 12:07:39 +02:00
public static function set_reading_mode ( $mode ) {
2010-04-13 05:16:57 +02:00
Versioned :: $reading_mode = $mode ;
}
/**
* Get the current reading mode .
* @ return string
*/
2012-09-19 12:07:39 +02:00
public static function get_reading_mode () {
2010-04-13 05:16:57 +02:00
return Versioned :: $reading_mode ;
}
2007-07-19 12:40:28 +02:00
/**
* Get the name of the 'live' stage .
* @ return string
*/
2012-09-19 12:07:39 +02:00
public static function get_live_stage () {
2007-07-19 12:40:28 +02:00
return " Live " ;
}
/**
* Get the current reading stage .
* @ return string
*/
2012-09-19 12:07:39 +02:00
public static function current_stage () {
2010-04-13 05:16:57 +02:00
$parts = explode ( '.' , Versioned :: get_reading_mode ());
if ( $parts [ 0 ] == 'Stage' ) return $parts [ 1 ];
2007-07-19 12:40:28 +02:00
}
/**
* Get the current archive date .
* @ return string
*/
2012-09-19 12:07:39 +02:00
public static function current_archived_date () {
2010-04-13 05:16:57 +02:00
$parts = explode ( '.' , Versioned :: get_reading_mode ());
if ( $parts [ 0 ] == 'Archive' ) return $parts [ 1 ];
2007-07-19 12:40:28 +02:00
}
/**
* Set the reading stage .
* @ param string $stage New reading stage .
*/
2012-09-19 12:07:39 +02:00
public static function reading_stage ( $stage ) {
2010-04-13 05:16:57 +02:00
Versioned :: set_reading_mode ( 'Stage.' . $stage );
2007-07-19 12:40:28 +02:00
}
/**
* Set the reading archive date .
* @ param string $date New reading archived date .
*/
2012-09-19 12:07:39 +02:00
public static function reading_archived_date ( $date ) {
2010-04-13 05:16:57 +02:00
Versioned :: set_reading_mode ( 'Archive.' . $date );
2007-07-19 12:40:28 +02:00
}
2010-04-13 05:16:57 +02:00
2007-07-19 12:40:28 +02:00
/**
* Get a singleton instance of a class in the given stage .
2008-10-16 12:14:47 +02:00
*
2007-07-19 12:40:28 +02:00
* @ param string $class The name of the class .
* @ param string $stage The name of the stage .
* @ param string $filter A filter to be inserted into the WHERE clause .
2008-10-16 12:14:47 +02:00
* @ param boolean $cache Use caching .
* @ param string $orderby A sort expression to be inserted into the ORDER BY clause .
2007-07-19 12:40:28 +02:00
* @ return DataObject
*/
2012-09-19 12:07:39 +02:00
public static function get_one_by_stage ( $class , $stage , $filter = '' , $cache = true , $sort = '' ) {
2012-05-28 11:13:42 +02:00
// TODO: No identity cache operating
$items = self :: get_by_stage ( $class , $stage , $filter , $sort , null , 1 );
return $items -> First ();
}
2007-07-19 12:40:28 +02:00
2008-12-17 23:38:47 +01:00
/**
* Gets the current version number of a specific record .
*
* @ param string $class
* @ param string $stage
* @ param int $id
* @ param boolean $cache
* @ return int
*/
2012-09-19 12:07:39 +02:00
public static function get_versionnumber_by_stage ( $class , $stage , $id , $cache = true ) {
2008-12-17 23:38:47 +01:00
$baseClass = ClassInfo :: baseDataClass ( $class );
$stageTable = ( $stage == 'Stage' ) ? $baseClass : " { $baseClass } _ { $stage } " ;
// cached call
if ( $cache && isset ( self :: $cache_versionnumber [ $baseClass ][ $stage ][ $id ])) {
return self :: $cache_versionnumber [ $baseClass ][ $stage ][ $id ];
}
2011-03-21 07:53:09 +01:00
2008-12-17 23:38:47 +01:00
// get version as performance-optimized SQL query (gets called for each page in the sitetree)
2009-02-18 04:12:39 +01:00
$version = DB :: query ( " SELECT \" Version \" FROM \" $stageTable\ " WHERE \ " ID \" = $id " ) -> value ();
2008-12-17 23:38:47 +01:00
// cache value (if required)
if ( $cache ) {
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 ;
}
return $version ;
}
2009-02-02 00:49:53 +01:00
/**
* 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 .
*/
2012-09-19 12:07:39 +02:00
public static function prepopulate_versionnumber_cache ( $class , $stage , $idList = null ) {
2009-02-02 00:49:53 +01:00
$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 , E_USER_ERROR );
2009-03-09 06:14:45 +01:00
$filter = " WHERE \" ID \" IN( " . implode ( " , " , $idList ) . " ) " ;
2009-02-02 00:49:53 +01:00
}
$baseClass = ClassInfo :: baseDataClass ( $class );
$stageTable = ( $stage == 'Stage' ) ? $baseClass : " { $baseClass } _ { $stage } " ;
2009-02-18 04:12:39 +01:00
$versions = DB :: query ( " SELECT \" ID \" , \" Version \" FROM \" $stageTable\ " $filter " )->map();
2009-02-02 00:49:53 +01:00
foreach ( $versions as $id => $version ) {
self :: $cache_versionnumber [ $baseClass ][ $stage ][ $id ] = $version ;
}
}
2008-10-16 12:14:47 +02:00
/**
* Get a set of class instances by the given stage .
*
* @ param string $class The name of the class .
* @ param string $stage The name of the stage .
* @ param string $filter A filter to be inserted into the WHERE clause .
* @ param string $sort A sort expression to be inserted into the ORDER BY clause .
* @ param string $join A join expression , such as LEFT JOIN or INNER JOIN
* @ param int $limit A limit on the number of records returned from the database .
2011-10-26 08:09:04 +02:00
* @ param string $containerClass The container class for the result set ( default is DataList )
* @ return SS_List
2008-10-16 12:14:47 +02:00
*/
2012-09-19 12:07:39 +02:00
public static function get_by_stage ( $class , $stage , $filter = '' , $sort = '' , $join = '' , $limit = '' , $containerClass = 'DataList' ) {
2008-10-16 12:14:47 +02:00
$result = DataObject :: get ( $class , $filter , $sort , $join , $limit , $containerClass );
2009-11-22 06:16:38 +01:00
$dq = $result -> dataQuery ();
$dq -> setQueryParam ( 'Versioned.mode' , 'stage' );
$dq -> setQueryParam ( 'Versioned.stage' , $stage );
2007-07-19 12:40:28 +02:00
return $result ;
}
2012-09-19 12:07:39 +02:00
public function deleteFromStage ( $stage ) {
2010-04-13 05:16:57 +02:00
$oldMode = Versioned :: get_reading_mode ();
Versioned :: reading_stage ( $stage );
2010-10-15 04:55:45 +02:00
$clone = clone $this -> owner ;
$result = $clone -> delete ();
2010-04-13 05:16:57 +02:00
Versioned :: set_reading_mode ( $oldMode );
2010-10-15 04:56:05 +02:00
// Fix the version number cache (in case you go delete from stage and then check ExistsOnLive)
$baseClass = ClassInfo :: baseDataClass ( $this -> owner -> class );
self :: $cache_versionnumber [ $baseClass ][ $stage ][ $this -> owner -> ID ] = null ;
2007-07-19 12:40:28 +02:00
return $result ;
}
2012-09-19 12:07:39 +02:00
public function writeToStage ( $stage , $forceInsert = false ) {
2010-04-13 05:16:57 +02:00
$oldMode = Versioned :: get_reading_mode ();
Versioned :: reading_stage ( $stage );
2007-07-19 12:40:28 +02:00
$result = $this -> owner -> write ( false , $forceInsert );
2010-04-13 05:16:57 +02:00
Versioned :: set_reading_mode ( $oldMode );
2007-07-19 12:40:28 +02:00
return $result ;
}
2011-03-21 07:53:09 +01:00
2011-03-23 04:02:10 +01:00
/**
* 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
*/
2012-09-19 12:07:39 +02:00
public function doRollbackTo ( $version ) {
2011-03-23 04:02:10 +01:00
$this -> publish ( $version , " Stage " , true );
$this -> owner -> writeWithoutVersion ();
}
2007-07-19 12:40:28 +02:00
/**
2008-12-17 23:38:47 +01:00
* Return the latest version of the given page .
*
* @ return DataObject
2007-07-19 12:40:28 +02:00
*/
2012-09-19 12:07:39 +02:00
public static function get_latest_version ( $class , $id ) {
2011-03-21 07:53:09 +01:00
$baseClass = ClassInfo :: baseDataClass ( $class );
2012-06-28 03:00:40 +02:00
$list = DataList :: create ( $baseClass ) -> where ( " \" $baseClass\ " . \ " RecordID \" = $id " );
2012-05-28 11:13:42 +02:00
$list -> dataQuery () -> setQueryParam ( " Versioned.mode " , " latest_versions " );
return $list -> First ();
2007-07-19 12:40:28 +02:00
}
2011-08-26 04:05:54 +02:00
/**
* Returns whether the current record is the latest one .
*
* @ todo Performance - could do this directly via SQL .
*
* @ see get_latest_version ()
* @ see latestPublished
*
* @ return bool
*/
2012-09-19 12:07:39 +02:00
public function isLatestVersion () {
2011-08-26 04:05:54 +02:00
$version = self :: get_latest_version ( $this -> owner -> class , $this -> owner -> ID );
return ( $version -> Version == $this -> owner -> Version );
}
2009-05-01 05:49:34 +02:00
/**
2012-03-19 03:27:52 +01:00
* Return the equivalent of a DataList :: create () call , querying the latest
2009-05-01 05:49:34 +02:00
* version of each page stored in the ( class ) _versions tables .
*
* In particular , this will query deleted records as well as active ones .
*/
2012-09-19 12:07:39 +02:00
public static function get_including_deleted ( $class , $filter = " " , $sort = " " ) {
2012-05-28 11:13:42 +02:00
$list = DataList :: create ( $class ) -> where ( $filter ) -> sort ( $sort );
$list -> dataQuery () -> setQueryParam ( " Versioned.mode " , " latest_versions " );
return $list ;
2009-05-01 05:49:34 +02:00
}
2007-07-19 12:40:28 +02:00
2008-12-17 23:38:47 +01:00
/**
2012-06-27 16:03:08 +02:00
* 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 .
*
2008-12-17 23:38:47 +01:00
* @ return DataObject
*/
2012-09-19 12:07:39 +02:00
public static function get_version ( $class , $id , $version ) {
2011-03-21 07:53:09 +01:00
$baseClass = ClassInfo :: baseDataClass ( $class );
2012-06-28 03:00:40 +02:00
$list = DataList :: create ( $baseClass ) -> where ( " \" $baseClass\ " . \ " RecordID \" = $id " ) -> where ( " \" $baseClass\ " . \ " Version \" = " . ( int ) $version );
2012-05-28 11:13:42 +02:00
$list -> dataQuery () -> setQueryParam ( 'Versioned.mode' , 'all_versions' );
return $list -> First ();
2007-07-19 12:40:28 +02:00
}
2008-12-17 23:38:47 +01:00
/**
2011-03-21 07:53:09 +01:00
* Return a list of all versions for a given id
* @ return DataList
2008-12-17 23:38:47 +01:00
*/
2012-09-19 12:07:39 +02:00
public static function get_all_versions ( $class , $id ) {
2012-05-28 11:13:42 +02:00
$baseClass = ClassInfo :: baseDataClass ( $class );
$list = DataList :: create ( $class ) -> where ( " \" $baseClass\ " . \ " RecordID \" = $id " );
$list -> dataQuery () -> setQueryParam ( 'Versioned.mode' , 'all_versions' );
return $list ;
2007-07-19 12:40:28 +02:00
}
2012-09-19 12:07:39 +02:00
public function contentcontrollerInit ( $controller ) {
2007-08-15 12:13:27 +02:00
self :: choose_site_stage ();
}
2012-09-19 12:07:39 +02:00
public function modelascontrollerInit ( $controller ) {
2007-08-15 12:13:27 +02:00
self :: choose_site_stage ();
}
2010-04-13 05:16:57 +02:00
protected static $reading_mode = null ;
2008-11-02 21:04:10 +01:00
2012-09-19 12:07:39 +02:00
public function updateFieldLabels ( & $labels ) {
2012-04-14 00:16:22 +02:00
$labels [ 'Versions' ] = _t ( 'Versioned.has_many_Versions' , 'Versions' , 'Past Versions of this page' );
2008-11-02 21:04:10 +01:00
}
2008-12-17 23:38:47 +01:00
2012-09-19 12:07:39 +02:00
public function flushCache () {
2008-12-17 23:38:47 +01:00
self :: $cache_versionnumber = array ();
}
2010-04-12 04:40:50 +02:00
/**
* Return a piece of text to keep DataObject cache keys appropriately specific
*/
2012-09-19 12:07:39 +02:00
public function cacheKeyComponent () {
2011-03-21 07:53:09 +01:00
return 'versionedmode-' . self :: get_reading_mode ();
2010-04-12 04:40:50 +02:00
}
2007-07-19 12:40:28 +02:00
}
/**
* Represents a single version of a record .
2011-08-26 04:05:54 +02:00
*
2012-04-12 08:02:46 +02:00
* @ package framework
2008-02-25 03:10:37 +01:00
* @ subpackage model
2011-08-26 04:05:54 +02:00
*
2008-02-25 03:10:37 +01:00
* @ see Versioned
2007-07-19 12:40:28 +02:00
*/
class Versioned_Version extends ViewableData {
protected $record ;
protected $object ;
2012-09-19 12:07:39 +02:00
public function __construct ( $record ) {
2007-07-19 12:40:28 +02:00
$this -> record = $record ;
$record [ 'ID' ] = $record [ 'RecordID' ];
$className = $record [ 'ClassName' ];
2012-08-29 04:30:10 +02:00
$this -> object = ClassInfo :: exists ( $className ) ? new $className ( $record ) : new DataObject ( $record );
2007-07-19 12:40:28 +02:00
$this -> failover = $this -> object ;
parent :: __construct ();
}
2012-09-19 12:07:39 +02:00
public function PublishedClass () {
2007-07-19 12:40:28 +02:00
return $this -> record [ 'WasPublished' ] ? 'published' : 'internal' ;
}
2012-09-19 12:07:39 +02:00
public function Author () {
2007-07-19 12:40:28 +02:00
return DataObject :: get_by_id ( " Member " , $this -> record [ 'AuthorID' ]);
}
2012-09-19 12:07:39 +02:00
public function Publisher () {
2007-07-19 12:40:28 +02:00
if ( ! $this -> record [ 'WasPublished' ] )
return null ;
return DataObject :: get_by_id ( " Member " , $this -> record [ 'PublisherID' ]);
}
2012-09-19 12:07:39 +02:00
public function Published () {
2007-07-19 12:40:28 +02:00
return ! empty ( $this -> record [ 'WasPublished' ] );
}
}