Revision Behavior - Revision control made easy

By Alexander Morland (alkemann)
Take full control of any changes your users makes, while also giving them features like undo. Keep a history of previous versions of any database model, allowing you to undo, revert to an older version (or a specific time), manage and inspect changes and even get a difference array for seeing changes over time to any (or all) fields.
RevisionBehavior is a solution for adding undo and other versioning functionality
to your database models. It is set up for easy application to your project,
ease of use and to not get in the way of your other model activity.
It is also intended to work well with it's sibling, LogableBehavior.

Feature list

  1. Easy to install
  2. Automagically save revision on model save
  3. Able to ignore model saves which only contain certain fields
  4. Limit number of revisions to keep, will delete oldest
  5. Undo functionality (or update to any revision directly)
  6. Revert to a datetime (and even do so cascading)
  7. Get a diff model array to compare two or more revisions
  8. Inspect any or all revisions of a model

Install instructions

  1. Place the newest version of RevisionBehavior in your app/models/behaviors folder
  2. Add the behavior to AppModel (or single models if you prefer)
  3. For each model that you want revision for, create a shadow table
  4. Behavior will gracefully do nothing for models that has behavior, but not shadow table
  5. If adding Revision 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 rev_[normal table name]. If you wish to change the prefix you may
do so in the property called $revision_prefix found in the behavior. Also by default the behavior expects
the shadow 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.

  1. The 'id' field should NOT be the primary key, nor auto increment.
  2. Add the fields 'version_id' (int, primary key, autoincrement) and 'version_created' (datetime).
  3. Skipp fields that should not be saved in shadowtable (lft,right,weight for instance).

Configuration

When adding 'Revision' the a model's actsAs array, you may configure the behavior with these options:

  1. limit : int number of revisions to keep, must be at least 2 (as current is 1).
  2. ignore : array containing the name of fields to ignore.
  3. auto : boolean when false the behavior will NOT generate revisions in afterSave.
  4. 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.

Page 2: Examples

Comments 885

CakePHP Team Comments Author Comments
 

Comment

1 Looks great!

This is exactly what I needed, thanks!
Posted Dec 19, 2008 by Aidan Lister
 

Question

2 Related models

Looks nice.

Does it automagically take snapshots of the related models as well, i.e the true current state of the model?

E.g., I have a Category model [id, name] that hasMany Subcategories [id, category_id, name]. Can the RevisionBehavior answer questions like "Which Subcategories did a Category have at a specific time?"
Posted Dec 19, 2008 by Stefan
 

Comment

3 Re: Related models

It doesnt have a function that answer that question, but since it stores not only old data, but current data as well, you can answer that question by asking for all subcategories with category_id = X and version_created between z and y.
Posted Dec 19, 2008 by Alexander Morland
 

Comment

4 New version

RevisionBehavior updated to version 1.1 on the SVN checkout: http://code.google.com/p/alkemann/source/checkout
Direct link to update code : http://code.google.com/p/alkemann/source/browse/trunk/models/behaviors/revision.php
Important notice : Due to a new requirement of having the ShadowModel inherit the table prefix from the Model, the internal logic of building a shadowtable name changed. In this process a new naming convention was establish, that I believe is an improvement. So the new rule is :

[any prefix][model_table_name]_revs
Example: users => users_revs, project_posts => project_posts_revs, model_prefix_comments => model_prefix_comments_revs

(Make sure you are using version 1.1 or later before renaming your tables to this)
Posted Dec 26, 2008 by Alexander Morland
 

Comment

5 Great job

This behavior is really fantastic. Seems to be very useful and polished. This is a great addition to the tool belt.
Posted Jan 1, 2009 by Giuliano Barberi
 

Comment

6 Version 2

Version 2 is done, major features being it's integration with Logable behavior, and that it play's nice with Multilingual (who just achieved version 1 (no article yet)). Grab the new version at the google code site.
Posted Jan 15, 2009 by Alexander Morland
 

Comment

7 aftersave problem

in the function afterSave(), the line:

$data = $Model->find('first', array('contain'=> $habtm, 'conditions'=>array($Model->alias.'.'.$Model->primaryKey => $Model->id)));

returns the record before the save, not the record after the save. removing "'contain'=> $habtm" returns the correct record, but obviously includes all related models.

do you have any idea why this happens? using cake_1.2.1.8004 and revision behavior 2.0.3
Posted Mar 4, 2009 by Alexander
 

Comment

8 diff()

something i noticed, diff() always places the most recent version of the field in [0]. this means i cannot determine when the value was changed.

eg. field1 has init value of x1 and field2 has an init value of y1.
at time1, you change field1 to x2
at time2, you change field2 to y2

diff() returns
[0] => time2 {field1: x2, field2: y2} <--- x2 is here!?
[1] => time1 {field1: (blank), field2: (blank)}
[2] => init {field1: x1, field2: y1}

looking at this, there is no way to determine when field1 changed from x1 to x2 (since the value is blank for time1).

what it should return is
[0] => time2 {field1: (blank), field2: y2}
[1] => time1 {field1: x2, field2: (blank)}
[2] => init {field1: x1, field2: y1}
Posted Mar 4, 2009 by Alexander
 

Comment

9 Re: aftersave problem

Thanks for the feedback! Since you are using the svn version of the code could you please open a ticket on google code and attach a test case or at least more details, as we are unable to reproduce the problem you describe?

Your suggested changes to diff() are now implemented in svn
Posted Mar 6, 2009 by Ronny Vindenes