Permalink
Fetching contributors…
Cannot retrieve contributors at this time
executable file 984 lines (938 sloc) 33.5 KB
<?php
/**
* Revision Behavior 2.0.4
*
* Revision is a solution for adding undo and other versioning functionality
* to your database models. It is set up to be easy to apply to your project,
* to be easy to use and not get in the way of your other model activity.
* It is also intended to work well with it's sibling, LogableBehavior.
*
* Feature list :
*
* - Easy to install
* - Automagically save revision on model save
* - Able to ignore model saves which only contain certain fields
* - Limit number of revisions to keep, will delete oldest
* - Undo functionality (or update to any revision directly)
* - Revert to a datetime (and even do so cascading)
* - Get a diff model array to compare two or more revisions
* - Inspect any or all revisions of a model
* - Work with Tree Behavior
* - Includes beforeUndelete and afterUndelete callbacks
* - NEW As of 1.2 behavior will revision HABTM relationships (from one way)
*
* Install instructions :
*
* - Place the newest version of RevisionBehavior in your app/models/behaviors folder
* - Add the behavior to AppModel (or single models if you prefer)
* - Create a shadow table for each model that you want revision for.
* - Behavior will gracefully do nothing for models that has behavior, without table
* - If adding to an existing project, run the initializeRevisions() method once for each model.
*
* About shadow tables :
*
* You should make these AFTER you have baked your ordinary tables as they may interfer. By default
* the tables should be named "[prefix][model_table_name]_revs" If you wish to change the suffix you may
* do so in the property called $revision_suffix found bellow. Also by default the behavior expects
* the revision tables to be in the same dbconfig as the model, but you may change this on a per
* model basis with the useDbConfig config option.
*
* Add the same fields as in the live table, with 3 important differences.
* - The 'id' field should NOT be the primary key, nor auto increment
* - Add the fields 'version_id' (int, primary key, autoincrement) and
* 'version_created' (datetime)
* - Skipp fields that should not be saved in shadowtable (lft,right,weight for instance)
*
* Configuration :
*
* - 'limit' : number of revisions to keep, must be at least 2
* - 'ignore' : array containing the name of fields to ignore
* - 'auto' : boolean when false the behavior will NOT generate revisions in afterSave
* - 'useDbConfig' : string/null Name of dbConfig to use. Null to use Model's
*
* Limit functionality :
* The shadow table will save a revision copy when it saves live data, so the newest
* row in the shadow table will (in most cases) be the same as the current live data.
* The exception is when the ignore field functionality is used and the live data is
* updated only in those fields.
*
* Ignore field(s) functionality :
* If you wish to be able to update certain fields without generating new revisions,
* you can add those fields to the configuration ignore array. Any time the behavior's
* afterSave is called with just primary key and these fields, it will NOT generate
* a new revision. It WILL however save these fields together with other fields when it
* does save a revision. You will probably want to set up cron or otherwise call
* createRevision() to update these fields at some points.
*
* Auto functionality :
* By default the behavior will insert itself into the Model's save process by implementing
* beforeSave and afterSave. In afterSave, the behavior will save a new revision of the dataset
* that is now the live data. If you do NOT want this automatic behavior, you may set the config
* option 'auto' to false. Then the shadow table will remain empty unless you call createRevisions
* manually.
*
* HABTM revision feature :
* In order to do revision on HABTM relationship, add a text field to the main model's shadow table
* with the same name as the association, ie if Article habtm ArticleTag as Tag, add a field 'Tag'
* to articles_revs.
* NB! In version 1.2 and up to current, Using HABTM revision requires that both models uses this
* behavior (even if secondary model does not have a shadow table).
*
* 1.1.1 => 1.1.2 changelog
* - revisions() got new paramter: $include_current
* This now defaults to false, resulting in a change from 1.1.1. See tests
*
* 1.1.6 => 1.2
* - includes HABTM revision control (one way)
*
* 1.2 => 1.2.1
* - api change in revertToDate, added paramter for force delete if reverting to before earliest
*
* 1.2.6 => 1.2.7
* - api change: removed shadow(), changed revertToDate() to only recurse into related models that
* are dependent when cascade is true
*
* @author Ronny Vindenes
* @author Alexander 'alkemann' Morland
* @license MIT
* @modifed 27. march 2009
* @version 2.0.4
*/
class RevisionBehavior extends ModelBehavior {
/**
* Behavior settings
*
* @access public
* @var array
*/
public $settings = array();
/**
* Shadow table prefix
* Only change this value if it causes table name crashes
*
* @access private
* @var string
*/
private $revision_suffix = '_versions';
/**
* Defaul setting values
*
* @access private
* @var array
*/
private $defaults = array(
'limit' => false,
'auto' => true,
'ignore' => array(),
'useDbConfig' => null,
'model' => null
);
/**
* Old data, used to detect changes
*
* @var array
*/
private $oldData = array();
/**
* Configure the behavior through the Model::actsAs property
*
* @param object $Model
* @param array $config
*/
public function setup(&$Model, $config = null) {
if (is_array($config)) {
$this->settings[$Model->alias] = array_merge($this->defaults, $config);
} else {
$this->settings[$Model->alias] = $this->defaults;
}
$this->createShadowModel($Model);
$Model->Behaviors->attach('Containable');
}
/**
* Manually create a revision of the current record of Model->id
*
* @example $this->Post->id = 5; $this->Post->createRevision();
* @param object $Model
* @return boolean success
*/
public function createRevision(&$Model) {
if (! $Model->id) {
trigger_error('RevisionBehavior: Model::id must be set', E_USER_WARNING); return null;
}
if (!$Model->ShadowModel) {
trigger_error('RevisionBehavior: ShadowModel doesnt exist.', E_USER_WARNING);
return false;
}
$habtm = array();
$all_habtm = $Model->getAssociated('hasAndBelongsToMany');
foreach ($all_habtm as $assocAlias) {
if (isset($Model->ShadowModel->_schema[$assocAlias])) {
$habtm[] = $assocAlias;
}
}
$data = $Model->find('first', array(
'conditions'=>array($Model->alias.'.'.$Model->primaryKey => $Model->id),
'contain' => $habtm
));
$Model->ShadowModel->create($data);
$Model->ShadowModel->set('version_created', date('Y-m-d H:i:s'));
foreach ($habtm as $assocAlias) {
$foreign_keys = Set::extract($data,'/'.$assocAlias.'/'.$Model->{$assocAlias}->primaryKey);
$Model->ShadowModel->set($assocAlias, implode(',',$foreign_keys));
}
return $Model->ShadowModel->save();
}
/**
* Returns an array that maps to the Model, only with multiple values for fields that has been changed
*
* @example $this->Post->id = 4; $changes = $this->Post->diff();
* @example $this->Post->id = 4; $my_changes = $this->Post->diff(null,nul,array('conditions'=>array('user_id'=>4)));
* @example $this->Post->id = 4; $difference = $this->Post->diff(45,192);
* @param Object $Model
* @param int $from_version_id
* @param int $to_version_id
* @param array $options
* @return array
*/
public function diff(&$Model, $from_version_id = null, $to_version_id = null, $options = array()) {
if (! $Model->id) {
trigger_error('RevisionBehavior: Model::id must be set', E_USER_WARNING); return null;
}
if (!$Model->ShadowModel) {
trigger_error('RevisionBehavior: ShadowModel doesnt exist.', E_USER_WARNING);
return null;
}
if (isset($options['conditions'])) {
$conditions = am($options['conditions'],array($Model->primaryKey => $Model->id));
} else {
$conditions = array( $Model->primaryKey => $Model->id);
}
if (is_numeric($from_version_id) || is_numeric($to_version_id)) {
if (is_numeric($from_version_id) && is_numeric($to_version_id)) {
$conditions['version_id'] = array($from_version_id,$to_version_id);
if ($Model->ShadowModel->find('count',array('conditions'=>$conditions)) < 2) {
return false;
}
} else {
if (is_numeric($from_version_id)) {
$conditions['version_id'] = $from_version_id;
} else {
$conditions['version_id'] = $to_version_id;
}
if ($Model->ShadowModel->find('count',array('conditions'=>$conditions)) < 1) {
return false;
}
}
}
$conditions = array($Model->primaryKey => $Model->id);
if (is_numeric($from_version_id)) {
$conditions['version_id >='] = $from_version_id;
}
if (is_numeric($to_version_id)) {
$conditions['version_id <='] = $to_version_id;
}
$options['conditions'] = $conditions;
$all = $this->revisions($Model,$options,true);
if (sizeof($all) == 0) {
return null;
}
$unified = array();
$keys = array_keys($all[0][$Model->alias]);
foreach ($keys as $field) {
$all_values = Set::extract($all,'/'.$Model->alias.'/'.$field);
$all_values = array_reverse(array_unique(array_reverse($all_values,true)),true);
if (sizeof($all_values) == 1) {
$unified[$field] = reset($all_values);
} else {
$unified[$field] = $all_values;
}
}
return array($Model->alias => $unified);
}
/**
* Will create a current revision of all rows in Model, if none exist.
* Use this if you add the revision to a model that allready has data in
* the DB.
* If you have large tables or big/many fields, use $limit to reduce the
* number of rows that is run at once.
*
* @example $this->Post->initializeRevisions();
* @param object $Model
* @param int $limit number of rows to initialize in one go
* @return boolean
*/
public function initializeRevisions(&$Model, $limit = 100) {
if (!$Model->ShadowModel) {
trigger_error('RevisionBehavior: ShadowModel doesnt exist.', E_USER_WARNING);
return false;
}
if ($Model->ShadowModel->useTable == false) {
trigger_error('RevisionBehavior: Missing shadowtable : '.$Model->table.$this->suffix, E_USER_WARNING);
return null;
}
if ($Model->ShadowModel->find('count') != 0) {
return false;
}
$count = $Model->find('count');
if ($limit < $count) {
$remaining = $count;
for ($p = 1; true; $p++ ) {
$this->init($Model, $p, $limit);
$remaining = $remaining - $limit;
if ($remaining <= 0) {
break;
}
}
} else {
$this->init($Model, 1, $count);
}
return true;
}
/**
* saves revisions for rows matching page and limit given
*
* @param object $Model
* @param int $page
* @param int $limit
*/
private function init(&$Model, $page, $limit) {
$habtm = array();
$all_habtm = $Model->getAssociated('hasAndBelongsToMany');
foreach ($all_habtm as $assocAlias) {
if (isset($Model->ShadowModel->_schema[$assocAlias])) {
$habtm[] = $assocAlias;
}
}
$all = $Model->find('all', array(
'limit' => $limit,
'page' => $page,
'contain' => $habtm
));
$version_created = date('Y-m-d H:i:s');
foreach ($all as $data) {
$Model->ShadowModel->create($data);
$Model->ShadowModel->set('version_created', $version_created);
$Model->ShadowModel->save();
}
}
/**
* Finds the newest revision, including the current one.
* Use with caution, the live model may be different depending on the usage
* of ignore fields.
*
* @example $this->Post->id = 6; $newest_revision = $this->Post->newest();
* @param object $Model
* @param array $options
* @return array
*/
public function newest(&$Model, $options = array()) {
if (! $Model->id) {
trigger_error('RevisionBehavior: Model::id must be set', E_USER_WARNING); return null;
}
if (!$Model->ShadowModel) {
trigger_error('RevisionBehavior: ShadowModel doesnt exist.', E_USER_WARNING);
return null;
}
if (isset($options['conditions'])) {
$options['conditions'] = am($options['conditions'],array($Model->alias.'.'.$Model->primaryKey => $Model->id));
} else {
$options['conditions'] = array( $Model->alias.'.'.$Model->primaryKey => $Model->id);
}
return $Model->ShadowModel->find('first',$options);
}
/**
* Find the oldest revision for the current Model->id
* If no limit is used on revision and revision has been enabled for the model
* since start, this call will return the original first record.
*
* @example $this->Post->id = 2; $original = $this->Post->oldest();
* @param object $Model
* @param array $options
* @return array
*/
public function oldest(&$Model, $options = array()) {
if (! $Model->id) {
trigger_error('RevisionBehavior: Model::id must be set', E_USER_WARNING); return null;
}
if (!$Model->ShadowModel) {
trigger_error('RevisionBehavior: ShadowModel doesnt exist.', E_USER_WARNING);
return null;
}
if (isset($options['conditions'])) {
$options['conditions'] = am($options['conditions'],array($Model->primaryKey => $Model->id));
} else {
$options['conditions'] = array( $Model->primaryKey => $Model->id);
}
$options['order'] = 'version_created ASC, version_id ASC';
return $Model->ShadowModel->find('first',$options);
}
/**
* Find the second newest revisions, including the current one.
*
* @example $this->Post->id = 6; $undo_revision = $this->Post->previous();
* @param object $Model
* @param array $options
* @return array
*/
public function previous(&$Model, $options = array()) {
if (! $Model->id) {
trigger_error('RevisionBehavior: Model::id must be set', E_USER_WARNING); return null;
}
if (!$Model->ShadowModel) {
trigger_error('RevisionBehavior: ShadowModel doesnt exist.', E_USER_WARNING);
return false;
}
$options['limit'] = 1;
$options['page'] = 2;
if (isset($options['conditions'])) {
$options['conditions'] = am($options['conditions'],array($Model->primaryKey => $Model->id));
} else {
$options['conditions'] = array( $Model->primaryKey => $Model->id);
}
$revisions = $Model->ShadowModel->find('all',$options);
if (!$revisions) {
return null;
}
return $revisions[0];
}
/**
* Revert all rows matching conditions to given date.
* Model rows outside condition or not edited will not be affected. Edits since date
* will be reverted and rows created since date deleted.
*
* @param object $Model
* @param array $options 'conditions','date'
* @return boolean success
*/
public function revertAll(&$Model, $options = array()) {
if (!$Model->ShadowModel) {
trigger_error('RevisionBehavior: ShadowModel doesnt exist.', E_USER_WARNING);
return false;
}
if (empty($options) || !isset($options['date'])) {
return FALSE;
}
if (!isset($options['conditions'])) {
$options['conditions'] = array();
}
// leave model rows out side of condtions alone
// leave model rows not edited since date alone
$all = $Model->find('all',array('conditions'=>$options['conditions'],'fields'=>$Model->primaryKey));
$allIds = Set::extract($all,'/'.$Model->alias.'/'.$Model->primaryKey);
$cond = $options['conditions'];
$cond['version_created <'] = $options['date'];
$created_before_date = $Model->ShadowModel->find('all',array(
'order' => $Model->primaryKey,
'conditions' => $cond,
'fields' => array('version_id',$Model->primaryKey)
));
$created_before_dateIds = Set::extract($created_before_date,'/'.$Model->alias.'/'.$Model->primaryKey);
$deleteIds = array_diff($allIds,$created_before_dateIds);
// delete all Model rows where there are only version_created later than date
$Model->deleteAll(array($Model->alias.'.'.$Model->primaryKey => $deleteIds),false,true);
unset($cond['version_created <']);
$cond['version_created >='] = $options['date'];
$created_after_date = $Model->ShadowModel->find('all',array(
'order' => $Model->primaryKey,
'conditions' => $cond,
'fields' => array('version_id',$Model->primaryKey)
));
$created_after_dateIds = Set::extract($created_after_date,'/'.$Model->alias.'/'.$Model->primaryKey);
$updateIds = array_diff($created_after_dateIds,$deleteIds);
$revertSuccess = true;
// update model rows that have version_created earlier than date to latest before date
foreach ($updateIds as $mid) {
$Model->id = $mid;
if ( ! $Model->revertToDate($options['date']) ) {
$revertSuccess = false;
}
}
return $revertSuccess;
}
/**
* Revert current Model->id to the given revision id
* Will return false if version id is invalid or save fails
*
* @example $this->Post->id = 3; $this->Post->revertTo(12);
* @param object $Model
* @param int $version_id
* @return boolean
*/
public function revertTo(&$Model, $version_id) {
if (! $Model->id) {
trigger_error('RevisionBehavior: Model::id must be set', E_USER_WARNING); return null;
}
if (!$Model->ShadowModel) {
trigger_error('RevisionBehavior: ShadowModel doesnt exist.', E_USER_WARNING);
return false;
}
$data = $Model->ShadowModel->find('first',array('conditions'=>array('version_id'=>$version_id)));
if ($data == false) {
return false;
}
foreach ($Model->getAssociated('hasAndBelongsToMany') as $assocAlias) {
if (isset($Model->ShadowModel->_schema[$assocAlias])) {
$data[$assocAlias][$assocAlias] = explode(',',$data[$Model->alias][$assocAlias]);
}
}
return $Model->save($data);
}
/**
* Revert to the oldest revision after the given datedate.
* Will cascade to hasOne and hasMany associeted models if $cascade is true.
* Will return false if no change is made on the main model
*
* @example $this->Post->id = 3; $this->Post->revertToDate(date('Y-m-d H:i:s',strtotime('Yesterday')));
* @example $this->Post->id = 4; $this->Post->revertToDate('2008-09-01',true);
* @param object $Model
* @param string $datetime
* @param boolean $cascade
* @param boolean $force_delete
* @return boolean
*/
public function revertToDate(&$Model, $datetime, $cascade = false, $force_delete = false) {
if (! $Model->id) {
trigger_error('RevisionBehavior: Model::id must be set', E_USER_WARNING);
return null;
}
if ($cascade) {
$associated = array_merge($Model->hasMany, $Model->hasOne);
foreach ($associated as $assoc => $data) {
$ids = array();
$cascade = false;
/* Check if association has dependent children */
$depassoc = array_merge($Model->$assoc->hasMany, $Model->$assoc->hasOne);
foreach ($depassoc as $dep) {
if ($dep['dependent']) {
$cascade = true;
}
}
/* Query live data for children */
$children = $Model->$assoc->find('list', array('conditions'=>array($data['foreignKey']=>$Model->id),'recursive'=>-1));
if (!empty($children)) {
$ids = array_keys($children);
}
/* Query shadow table for deleted children */
$revision_children = $Model->$assoc->ShadowModel->find('all', array(
'fields'=>array('DISTINCT '.$Model->primaryKey),
'conditions'=>array(
$data['foreignKey']=>$Model->id,
'NOT' => array( $Model->primaryKey => $ids )
),
));
if (!empty($revision_children)) {
$ids = am($ids,Set::extract($revision_children,'/'.$assoc.'/'.$Model->$assoc->primaryKey));
}
/* Revert all children */
foreach ($ids as $id) {
$Model->$assoc->id = $id;
$Model->$assoc->revertToDate($datetime, $cascade, $force_delete);
}
}
}
if (empty($Model->ShadowModel)) {
return true;
}
$data = $Model->ShadowModel->find('first',array(
'conditions'=>array(
$Model->primaryKey => $Model->id,
'version_created <='=>$datetime
),
'order'=>'version_created ASC, version_id ASC'
));
/* If no previous version was found and revertToDate() was called with force_delete, then delete the live data, else leave it alone */
if ($data == false) {
if ($force_delete) {
$Model->logableAction['Revision'] = 'revertToDate('.$datetime.') delete';
return $Model->delete($Model->id);
}
return true;
}
$habtm = array();
foreach ($Model->getAssociated('hasAndBelongsToMany') as $assocAlias) {
if (isset($Model->ShadowModel->_schema[$assocAlias])) {
$habtm[] = $assocAlias;
}
}
$liveData = $Model->find('first', array(
'contain'=> $habtm,
'conditions'=>array($Model->alias.'.'.$Model->primaryKey => $Model->id)));
$Model->logableAction['Revision'] = 'revertToDate('.$datetime.') add';
if ($liveData) {
$Model->logableAction['Revision'] = 'revertToDate('.$datetime.') edit';
foreach ($Model->getAssociated('hasAndBelongsToMany') as $assocAlias) {
if (isset($Model->ShadowModel->_schema[$assocAlias])) {
$ids = Set::extract($liveData,'/'.$assocAlias.'/'.$Model->$assocAlias->primaryKey);
if (empty($ids) || is_string($ids)) {
$liveData[$Model->alias][$assocAlias] = '';
} else {
$liveData[$Model->alias][$assocAlias] = implode(',',$ids);
}
$data[$assocAlias][$assocAlias] = explode(',',$data[$Model->alias][$assocAlias]);
}
unset($liveData[$assocAlias]);
}
$changeDetected = false;
foreach ($liveData[$Model->alias] as $key => $value) {
if ( isset($data[$Model->alias][$key])) {
$old_value = $data[$Model->alias][$key];
} else {
$old_value = '';
}
if ($value != $old_value ) {
$changeDetected = true;
}
}
if (!$changeDetected) {
return true;
}
}
$auto = $this->settings[$Model->alias]['auto'];
$this->settings[$Model->alias]['auto'] = false;
$Model->ShadowModel->create($data,true);
$Model->ShadowModel->set('version_created', date('Y-m-d H:i:s'));
$Model->ShadowModel->save();
$Model->version_id = $Model->ShadowModel->id;
$success = $Model->save($data);
$this->settings[$Model->alias]['auto'] = $auto;
return $success;
}
/**
* Returns a comeplete list of revisions for the current Model->id.
* The options array may include Model::find parameters to narrow down result
* Alias for shadow('all',array('conditions'=>array($Model->primaryKey => $Model->id)));
*
* @example $this->Post->id = 4; $history = $this->Post->revisions();
* @example $this->Post->id = 4; $today = $this->Post->revisions(array('conditions'=>array('version_create >'=>'2008-12-10')));
* @param object $Model
* @param array $options
* @param boolean $include_current If true will include last saved (live) data
* @return array
*/
public function revisions(&$Model, $options = array(), $include_current = false) {
if (! $Model->id) {
trigger_error('RevisionBehavior: Model::id must be set', E_USER_WARNING); return null;
}
if (!$Model->ShadowModel) {
trigger_error('RevisionBehavior: ShadowModel doesnt exist.', E_USER_WARNING);
return null;
}
if (isset($options['conditions'])) {
$options['conditions'] = am($options['conditions'],array($Model->alias.'.'.$Model->primaryKey => $Model->id));
} else {
$options['conditions'] = array($Model->alias.'.'.$Model->primaryKey => $Model->id);
}
if ( $include_current == false ) {
$current = $this->newest($Model, array('fields'=>array($Model->alias.'.version_id',$Model->primaryKey)));
$options['conditions'][$Model->alias.'.version_id !='] = $current[$Model->alias]['version_id'];
}
return $Model->ShadowModel->find('all',$options);
}
/**
* Undoes an delete by saving the last revision to the Model
* Will return false if this Model->id exist in the live table.
* Calls Model::beforeUndelete and Model::afterUndelete
*
* @example $this->Post->id = 7; $this->Post->undelete();
* @param object $Model
* @return boolean
*/
public function undelete(&$Model) {
if (! $Model->id) {
trigger_error('RevisionBehavior: Model::id must be set', E_USER_WARNING); return null;
}
if (!$Model->ShadowModel) {
trigger_error('RevisionBehavior: ShadowModel doesnt exist.', E_USER_WARNING);
return false;
}
if ($Model->find('count',array(
'conditions'=>array($Model->primaryKey=>$Model->id),
'recursive'=>-1)) > 0) {
return false;
}
$data = $this->newest($Model);
if (!$data) {
return false;
}
$beforeUndeleteSuccess = true;
if (method_exists($Model,'beforeUndelete')) {
$beforeUndeleteSuccess = $Model->beforeUndelete();
}
if (!$beforeUndeleteSuccess) {
return false;
}
$model_id = $data[$Model->alias][$Model->primaryKey];
unset($data[$Model->alias][$Model->ShadowModel->primaryKey]);
$Model->create($data,true);
$auto_setting = $this->settings[$Model->alias]['auto'];
$this->settings[$Model->alias]['auto'] = false;
$save_success = $Model->save();
$this->settings[$Model->alias]['auto'] = $auto_setting;
if (!$save_success) {
return false;
}
$Model->updateAll(
array($Model->primaryKey => $model_id),
array($Model->primaryKey => $Model->id)
);
$Model->id = $model_id;
$Model->createRevision();
$afterUndeleteSuccess = true;
if (method_exists($Model,'afterUndelete')) {
$afterUndeleteSuccess = $Model->afterUndelete();
}
return $afterUndeleteSuccess;
}
/**
* Update to previous revision
*
* @example $this->Post->id = 2; $this->Post->undo();
* @param object $Model
* @return boolean
*/
public function undo(&$Model) {
if (! $Model->id) {
trigger_error('RevisionBehavior: Model::id must be set', E_USER_WARNING); return null;
}
if (!$Model->ShadowModel) {
trigger_error('RevisionBehavior: ShadowModel doesnt exist.', E_USER_WARNING);
return false;
}
$data = $this->previous($Model);
if ($data == false) {
$Model->logableAction['Revision'] = 'undo add';
$Model->delete($Model->id);
return false;
}
foreach ($Model->getAssociated('hasAndBelongsToMany') as $assocAlias) {
if (isset($Model->ShadowModel->_schema[$assocAlias])) {
$data[$assocAlias][$assocAlias] = explode(',',$data[$Model->alias][$assocAlias]);
}
}
$Model->logableAction['Revision'] = 'undo changes';
return $Model->save($data);
}
/**
* Calls create revision for all rows matching primary key list of $idlist
*
* @example $this->Model->updateRevisions(array(1,2,3));
* @param object $Model
* @param array $idlist
*/
public function updateRevisions(&$Model, $idlist = array()) {
if (!$Model->ShadowModel) {
trigger_error('RevisionBehavior: ShadowModel doesnt exist.', E_USER_WARNING);
return null;
}
foreach ($idlist as $id ) {
$Model->id = $id;
$Model->createRevision();
}
}
/**
* Causes revision for habtm associated models if that model does version control
* on their relationship. BeforeDelete identifies the related models that will need
* to do the revision update in afterDelete. Uses
*
* @param unknown_type $Model
*/
public function afterDelete(&$Model) {
if ($this->settings[$Model->alias]['auto'] === false) {
return true;
}
if (!$Model->ShadowModel) {
return true;
}
if (isset($this->deleteUpdates[$Model->alias]) && !empty($this->deleteUpdates[$Model->alias])) {
foreach ($this->deleteUpdates[$Model->alias] as $assocAlias => $assocIds) {
$Model->{$assocAlias}->updateRevisions($assocIds);
}
unset($this->deleteUpdates[$Model->alias]);
}
}
/**
* Will create a new revision if changes have been made in the models non-ignore fields.
* Also deletes oldest revision if limit is (active and) reached.
*
* @param object $Model
* @param boolean $created
* @return boolean
*/
public function afterSave(&$Model, $created) {
if ($this->settings[$Model->alias]['auto'] === false) {
return true;
}
if (!$Model->ShadowModel) {
return true;
}
if ($created) {
echo 'created';
$Model->ShadowModel->create($Model->data,true);
$Model->ShadowModel->set('id',$Model->id);
$Model->ShadowModel->set('version_created',date('Y-m-d H:i:s'));
foreach ($Model->data as $alias => $alias_data) {
if (isset($Model->ShadowModel->_schema[$alias])) {
if (isset($alias_data[$alias]) && !empty($alias_data[$alias])) {
$Model->ShadowModel->set($alias,implode(',',$alias_data[$alias]));
}
}
}
$success = $Model->ShadowModel->save();
var_dump($Model->ShadowModel);
$Model->version_id = $Model->ShadowModel->id;
return $success;
}
$habtm = array();
foreach ($Model->getAssociated('hasAndBelongsToMany') as $assocAlias) {
if (isset($Model->ShadowModel->_schema[$assocAlias])) {
$habtm[] = $assocAlias;
}
}
$data = $Model->find('first', array(
'contain'=> $habtm,
'conditions'=>array($Model->alias.'.'.$Model->primaryKey => $Model->id)));
$changeDetected = false;
foreach ($data[$Model->alias] as $key => $value) {
if ( isset($data[$Model->alias][$Model->primaryKey])
&& !empty($this->oldData[$Model->alias])
&& isset($this->oldData[$Model->alias][$Model->alias][$key])) {
$old_value = $this->oldData[$Model->alias][$Model->alias][$key];
} else {
$old_value = '';
}
if ($value != $old_value && !in_array($key,$this->settings[$Model->alias]['ignore'])) {
$changeDetected = true;
}
}
$Model->ShadowModel->create($data);
if (!empty($habtm)) {
foreach ($habtm as $assocAlias) {
if (in_array($assocAlias,$this->settings[$Model->alias]['ignore'])) {
continue;
}
$oldIds = Set::extract($this->oldData[$Model->alias],$assocAlias.'.{n}.id');
if (!isset($Model->data[$assocAlias])) {
$Model->ShadowModel->set($assocAlias, implode(',',$oldIds));
continue;
}
$currentIds = Set::extract($data,$assocAlias.'.{n}.id');
$id_changes = array_diff($currentIds,$oldIds);
if (!empty($id_changes)) {
$Model->ShadowModel->set($assocAlias, implode(',',$currentIds));
$changeDetected = true;
} else {
$Model->ShadowModel->set($assocAlias, implode(',',$oldIds));
}
}
}
unset($this->oldData[$Model->alias]);
if (!$changeDetected) {
return true;
}
$Model->ShadowModel->set('version_created', date('Y-m-d H:i:s'));
$Model->ShadowModel->save();
$Model->version_id = $Model->ShadowModel->id;
if (is_numeric($this->settings[$Model->alias]['limit'])) {
$conditions = array('conditions'=>array($Model->alias.'.'.$Model->primaryKey => $Model->id));
$count = $Model->ShadowModel->find('count', $conditions);
if ($count > $this->settings[$Model->alias]['limit']) {
$conditions['order'] = $Model->alias.'.version_created ASC, '.$Model->alias.'.version_id ASC';
$oldest = $Model->ShadowModel->find('first',$conditions);
$Model->ShadowModel->id = null;
$Model->ShadowModel->delete($oldest[$Model->alias][$Model->ShadowModel->primaryKey]);
}
}
return true;
}
/**
* Causes revision for habtm associated models if that model does version control
* on their relationship. BeforeDelete identifies the related models that will need
* to do the revision update in afterDelete.
*
* @param object $Model
* @return boolean
*/
public function beforeDelete(&$Model) {
if ($this->settings[$Model->alias]['auto'] === false) {
return true;
}
if (!$Model->ShadowModel) {
return true;
}
foreach ($Model->hasAndBelongsToMany as $assocAlias => $a) {
if (isset($Model->{$assocAlias}->ShadowModel->_schema[$Model->alias])) {
$joins = $Model->{$a['with']}->find('all',array(
'recursive' => -1,
'conditions' => array(
$a['foreignKey'] => $Model->id
)
));
$this->deleteUpdates[$Model->alias][$assocAlias] = Set::extract($joins,'/'.$a['with'].'/'.$a['associationForeignKey']);
}
}
return true;
}
/**
* Revision uses the beforeSave callback to remember the old data for comparison in afterSave
*
* @param object $Model
* @return boolean
*/
public function beforeSave(&$Model) {
if ($this->settings[$Model->alias]['auto'] === false) {
return true;
}
if (!$Model->ShadowModel) {
return true;
}
$Model->ShadowModel->create();
if (!isset($Model->data[$Model->alias][$Model->primaryKey]) && !$Model->id) {
return true;
}
$habtm = array();
foreach ($Model->getAssociated('hasAndBelongsToMany') as $assocAlias) {
if (isset($Model->ShadowModel->_schema[$assocAlias])) {
$habtm[] = $assocAlias;
}
}
$this->oldData[$Model->alias] = $Model->find('first', array(
'contain'=> $habtm,
'conditions'=>array($Model->alias.'.'.$Model->primaryKey => $Model->id)));
return true;
}
/**
* Returns a generic model that maps to the current $Model's shadow table.
*
* @param object $Model
* @return boolean
*/
private function createShadowModel(&$Model) {
if (is_null($this->settings[$Model->alias]['useDbConfig'])) {
$dbConfig = $Model->useDbConfig;
} else {
$dbConfig = $this->settings[$Model->alias]['useDbConfig'];
}
$db = & ConnectionManager::getDataSource($dbConfig);
if ($Model->useTable) {
$shadow_table = $Model->useTable;
} else {
$shadow_table = Inflector::tableize($Model->name);
}
$shadow_table = $shadow_table . $this->revision_suffix;
$prefix = $Model->tablePrefix ? $Model->tablePrefix : $db->config['prefix'];
$full_table_name = $prefix . $shadow_table;
$useShadowModel = $this->settings[$Model->alias]['model'];
if (is_string($useShadowModel) && App::import('model',$useShadowModel)) {
$Model->ShadowModel = new $useShadowModel(false, false, $dbConfig);
} else {
$Model->ShadowModel = new Model(false, $shadow_table, $dbConfig);
}
if ($Model->tablePrefix) {
$Model->ShadowModel->tablePrefix = $Model->tablePrefix;
}
$Model->ShadowModel->alias = $Model->alias;
$Model->ShadowModel->primaryKey = 'version_id';
$Model->ShadowModel->order = 'version_created DESC, version_id DESC';
return true;
}
}