Versionable Behavior
The versionable
behavior provides versioning capabilities to any ActiveRecord object. Using this behavior, you can:
- Revert an object to previous versions easily
- Track and browse history of the modifications of an object
- Keep track of the modifications in related objects
Basic Usage
In the schema.xml
, use the <behavior>
tag to add the versionable
behavior to a table:
<table name="book">
<column name="id" required="true" primaryKey="true" autoIncrement="true" type="integer" />
<column name="title" type="varchar" required="true" />
<behavior name="versionable" />
</table>
Rebuild your model, insert the table creation sql again, and you’re ready to go. The model now has one new column, version
, which stores the version number. It also has a new table, book_version
, which stores all the versions of all Book
objects, past and present. You won’t need to interact with this second table, since the behavior offers an easy-to-use API that takes care of all versioning actions from the main ActiveRecord object.
<?php
$book = new Book();
// automatic version increment
$book->setTitle('War and Peas');
$book->save();
echo $book->getVersion(); // 1
$book->setTitle('War and Peace');
$book->save();
echo $book->getVersion(); // 2
// reverting to a previous version
$book->toVersion(1);
echo $book->getTitle(); // 'War and Peas'
// saving a previous version creates a new one
$book->save();
echo $book->getVersion(); // 3
// checking differences between versions
print_r($book->compareVersions(1, 2));
// array(
// 'Title' => array(1 => 'War and Peas', 2 => 'War and Pace'),
// );
// deleting an object also deletes all its versions
$book->delete();
Adding details about each revision
For future reference, you probably need to record who edited an object, as well as when and why. To enable audit log capabilities, add the three following parameters to the <behavior>
tag:
<table name="book">
<column name="id" required="true" primaryKey="true" autoIncrement="true" type="integer" />
<column name="title" type="varchar" required="true" />
<behavior name="versionable">
<parameter name="log_created_at" value="true" />
<parameter name="log_created_by" value="true" />
<parameter name="log_comment" value="true" />
</behavior>
</table>
Rebuild your model, and you can now define an author name and a comment for each revision using the setVersionCreatedBy()
and setVersionComment()
methods, as follows:
<?php
$book = new Book();
$book->setTitle('War and Peas');
$book->setVersionCreatedBy('John Doe');
$book->setVersionComment('Book creation');
$book->save();
$book->setTitle('War and Peace');
$book->setVersionCreatedBy('John Doe');
$book->setVersionComment('Corrected typo on book title');
$book->save();
Retrieving revision history
<?php
// details about each revision are available for all versions of an object
$book->toVersion(1);
echo $book->getVersionCreatedBy(); // 'John Doe'
echo $book->getVersionComment(); // 'Book creation'
// besides, the behavior also logs the creation date for all versions
echo $book->getVersionCreatedAt(); // '2010-12-21 22:57:19'
// if you need to list the revision details, it is better to use the version object
// than the main object. The following requires only one database query:
foreach ($book->getAllVersions() as $bookVersion) {
echo sprintf("'%s', Version %d, updated by %s on %s (%s)\n",
$bookVersion->getTitle(),
$bookVersion->getVersion(),
$bookVersion->getVersionCreatedBy(),
$bookVersion->getVersionCreatedAt(),
$bookVersion->getVersionComment(),
);
}
// 'War and Peas', Version 1, updated by John Doe on 2010-12-21 22:53:02 (Book Creation)
// 'War and Peace', Version 2, updated by John Doe on 2010-12-21 22:57:19 (Corrected typo on book title)
Conditional versioning
You may not need a new version each time an object is created or modified. If you want to specify your own condition, just override the isVersioningNecessary()
method in your stub class. The behavior calls it behind the curtain each time you save()
the main object. No version is created if it returns false.
<?php
class Book extends BaseBook
{
public function isVersioningNecessary()
{
return $this->getISBN() !== null && parent::isVersioningNecessary();
}
}
$book = new Book();
$book->setTitle('Pride and Prejudice');
$book->save(); // book is saved, no new version is created
$book->setISBN('0553213105');
$book->save(): // book is saved, and a new version is created
Alternatively, you can choose to disable the automated creation of a new version at each save for all objects of a given model by calling the disableVersioning()
method on the Query class. In this case, you still have the ability to manually create a new version of an object, using the addVersion()
method on a saved object:
<?php
BookQuery::disableVersioning();
$book = new Book();
$book->setTitle('Pride and Prejudice');
$book->setVersion(1);
$book->save(); // book is saved, no new version is created
$book->addVersion(); // a new version is created
// you can reenable versioning using the Query static method enableVersioning()
BookQuery::enableVersioning();
Versioning Related objects
If a model uses the versionable behavior, and is related to another model also using the versionable behavior, then each object automatically keeps track of the modifications of related objects. This means that calling toVersion()
restores the state of the main object and of the related objects as well.
The following example assumes that both the Book
model and the Author
model are versionable - one Author
has many Books
:
<?php
$author = new Author();
$author->setFirstName('Leo');
$author->setLastName('Totoi');
$book = new Book();
$book->setTitle('War and Peas');
$book->setAuthor($author);
$book->save(); // version 1
$book->setTitle('War and Peace');
$book->save(); // version 2
$author->setLastName('Tolstoi');
$book->save(); // version 3
$book->toVersion(1);
echo $book->getTitle(); // 'War and Peas'
echo $book->getAuthor()->getLastName(); // 'Totoi'
$book->toVersion(3);
echo $book->getTitle(); // 'War and Peace'
echo $book->getAuthor()->getLastName(); // 'Tolstoi'
TipVersioning of related objects is only possible for simple foreign keys. Relationships based on composite foreign keys cannot preserve relation versionning for now.
Parameters
You can change the name of the column added by the behavior by setting the version_column
parameter. Propel only adds the column if it’s not already present, so you can easily customize this column by adding it manually to your schema:
<table name="book">
<column name="id" required="true" primaryKey="true" autoIncrement="true" type="integer" />
<column name="title" type="varchar" required="true" />
<column name="my_version_column" type="bigint" description="Version column" />
<behavior name="versionable">
<parameter name="version_column" value="my_version_column" />
</behavior>
</table>
<?php
$b = new Book();
$b->setTitle('War And Peace');
$b->save();
echo $b->getMyVersionColumn(); // 1
// For convenience and ease of use, Propel creates a getVersion() anyway
echo $b->getVersion(); // 1
You can also change the name of the version table by setting the version_table
parameter. Again, Propel automatically creates the table, unless it’s already present in the schema:
<table name="book">
<column name="id" required="true" primaryKey="true" autoIncrement="true" type="integer" />
<column name="title" type="varchar" required="true" />
<behavior name="versionable">
<parameter name="version_table" value="my_book_version" />
</behavior>
</table>
The audit log abilities need to be enabled in the schema as well:
<table name="book">
<column name="id" required="true" primaryKey="true" autoIncrement="true" type="integer" />
<column name="title" type="varchar" required="true" />
<behavior name="versionable">
<!-- Log the version creation date -->
<parameter name="log_created_at" value="true" />
<!-- Log the version creator name, using setVersionCreatedBy() -->
<parameter name="log_created_by" value="true" />
<!-- Log the version comment, using setVersionComment() -->
<parameter name="log_comment" value="true" />
</behavior>
</table>
Sometimes it is necessary to have indices from the origin table also in your version table, maybe
to fire queries against it. To achieve this you can either completely describe the <tableName>_version
in your
schema with all its necessary indices so the behavior won’t overwrite/re-add it or you use the parameter indices
.
<table name="book">
<column name="id" required="true" primaryKey="true" autoIncrement="true" type="integer" />
<column name="title" type="varchar" required="true" />
<index>
<index-column name="title"/>
</index>
<behavior name="versionable">
<parameter name="indices" value="true" />
</behavior>
</table>
Public API
ActiveRecord class
void save()
: Adds a new version to the object version history and increments theversion
propertyvoid delete()
: Deletes the object version historyboolean isVersioningNecessary(PropelPDO $con = null)
: Checks whether a new version needs to be savedBaseObject toVersion(integer $version_number)
: Populates the properties of the current object with values from the requested version. Beware that saving the object afterwards will create a new version (and not update the previous version).integer getLastVersionNumber(PropelPDO $con)
: Queries the database for the highest version number recorded for this objectboolean isLastVersion()
: Returns true if the current object is the last available versionVersion addVersion(PropelPDO $con)
: Creates a new Version record and saves it. To be used when isVersioningNecessary() is false. Beware that it doesn’t take care of incrementing the version number of the main object, and that the main object must be saved prior to calling this method.array getAllVersions(PropelPDO $con)
: Returns all Version objects related to the main object in a collectionVersion getOneVersion(integer $versionNumber PropelPDO $con)
: Returns a given version objectarray compareVersions(integer $version1, integer $version2)
: Returns an array of differences showing which parts of a resource changed between two versions-
BaseObject populateFromVersion(Version $version, PropelPDO $con)
: Populates an ActiveRecord object based on a Version object BaseObject setVersionCreatedBy(string $createdBy)
: Defines the author name for the revisionstring getVersionCreatedBy()
: Gets the author name for the revisionmixed getVersionCreatedAt()
: Gets the creation date for the revision (the behavior takes care of setting it)BaseObject setVersionComment(string $comment)
: Defines the comment for the revisionstring getVersionComment()
: Gets the comment for the revision
Query static methods
void enableVersioning()
: Enables versionning for all instances of the related ActiveRecord classvoid disableVersioning()
: Disables versionning for all instances of the related ActiveRecord classboolean isVersioningEnabled()
: Checks whether the versionnig is enabled