# sfPropelVersionableBehaviorPlugin plugin
The `sfPropelVersionableBehaviorPlugin` is a symfony plugin that provides versioning capabilities to any Propel object.
## Features
* Revert objects to previous versions easily
* Track and browse history of modifications on every object
* Conditional versioning
* Fully unit tested
## Installation
* Install the plugin
[sh]
> php symfony plugin-install http://plugins.symfony-project.com/sfPropelVersionableBehaviorPlugin
* Enable Propel behavior support in `propel.ini`:
propel.builder.AddBehaviors = true
* Add a `version` field to each of the model tables that you want to make versionable:
[xml]
<!-- schema.xml -->
<column name="version" type="INTEGER" />
or
# config/schema.yml
version: { type: integer }
Alternatively, you can choose another name that `version` and declare it in the behavior initialization. Even though, the behavior will still provide a `getVersion` and `setVersion` method for your versionable models.
* Rebuild your model and sql, insert the plugin tables to your database, and the new version column to your versionable tables:
[sh]
> php symfony propel-build-model
> php symfony propel-build-sql
> mysql -uroot -p mydb < data/sql/plugins.sfPropelVersionableBehaviorPlugin.lib.model.schema.sql
> mysql -uroot -p mydb -e 'ALTER TABLE `Article` ADD `version` INTEGER NOT NULL;'
* Enable the behavior for the Propel models that you want to extend. For instance, to extend an `Article` Propel class:
[php]
<?php
// lib/model/Article.php
class Article
{
}
sfPropelBehavior::add('Article', array('versionable'));
If the model uses a version column name diffeent than `version`, declare it here as the 'version' parameter of the behavior initialization:
[php]
sfPropelBehavior::add('Article', array('versionable' => array('columns' => array('version' => 'my_version_column'))));
* Clear the cache
[sh]
> php symfony cc
## Usage
### Reverting to a previous version
[php]
<?php
$article = new Article();
$article->setTitle('First version of article');
$article->save(); // $article->getVersion() == 1
$article->setTitle('Second version of article');
$article->save(); // $article->getVersion() == 2
$article->toVersion(1); // $article->getTitle() == 'First version of article'
$article->save(); // $article->getVersion() == 3
### Retrieving a resource version history
[php]
<?php
foreach ($article->getAllVersions() as $history_article)
{
echo sprintf("Version %d title : %s\n",
$history_article->getVersion(),
$history_article->getTitle()
);
}
/*
* Outputs:
*
* Version 1 title: First version of article
* Version 2 title: Second version of article
* Version 3 title: First version of article
*/
### Conditional versioning
You may not want to have a new version of resource created each time it is saved.
Just add a `versionConditionMet()` method to your stub class. It is called each time object's `save()`.
No version is created if it returns false.
Example :
[php]
<?php
// lib/model/Article.php
public function versionConditionMet()
{
return $this->getTitle() != 'do not version me';
}
[php]
<?php
$article = new Article();
$article->setTitle('New article');
$article->save(); // article is saved and a new version is created
$article->setTitle('do not version me');
$article->save(); // article is saved, no new version is created
It is possible to specify a different `versionConditionMet()` method name by defining it when registring behavior :
[php]
<?php
sfPropelBehavior::add('Article', array('versionable' => array('columns' => $columns_map, 'conditional' => 'myMethod')));
It is possible to change this method at runtime :
[php]
<?php
$previous_method = sfPropelVersionableBehavior::setVersionConditionMethod('myMethod');
Alternativley, you can choose to disable the automated creation of a new version at each save for all models by changing the application configuration:
# config/app.yml
all:
sfPropelVersionableBehaviorPlugin:
auto_versioning: false
In this case, you still have the way to manually create a new version of an object:
[php]
<?php
$article->setTitle('Please version me even though auto_versioning is false');
$article->addVersion();
$article->save(); // article is saved and a new version is created
### Customizing the behavior
During initialization, you can define the name of the 'version' column if different from 'version:
[php]
sfPropelBehavior::add('Article', array('versionable' => array('columns' => array(
'version' => 'my_version_column'
))));
If your model contains a 'title' column, the behavior will automatically copy it for reference into its internal `ResourceVersion` objects. But you can specify that the revision object title can come from another column:
[php]
sfPropelBehavior::add('Article', array('versionable' => array('columns' => array(
'title' => 'my_title_column'
))));
### Giving details about each revision
For future reference, you probably need to record who edited an object, as well as when and why. While editing your object, you can define an author name and a comment via the `setVersionCreatedBy()` and `setVersionComment()` methods, as follows:
[php]
<?php
$article = new Article();
$article->setTitle('Original title');
$article->setVersionCreatedBy('John Doe');
$article->setVersionComment('Article creation');
$article->save();
$article->setTitle('A much better title');
$article->setVersionCreatedBy('John Doe');
$article->setVersionComment('I didn\'t like the previous title so much');
$article->save();
Details about each revision are available in the object via the `getVersionCreatedBy` and `getVersionComment` methods. For instance, if you want to display a history of modifications, you can do as follows:
[php]
<?php
foreach ($article->getAllVersions() as $history_article)
{
echo sprintf("'%s', Version %d, updated by %s on %s (%s)\n",
$history_article->getTitle(),
$history_article->getVersion(),
$history_article->getVersionCreatedBy(),
$history_article->getVersionCreatedAt(),
$history_article->getVersionComment(),
);
}
/*
* Outputs:
*
* 'Original title', Version 1, updated by John Doe on 2008-02-08 09:25:12 (Article Creation)
* 'A much better title', Version 2, updated by John Doe on 2008-02-08 09:25:15 (I didn't like the previous title so much)
*/
Note: In the above example, the `getAllVersions()` method hydrates a list of `Article` objects while all you need is information about the revisions alone. A lighter way to do the same thing would consist of manipulating the `ResourceVersion` objects that you can get via `getAllResourceVersions()`:
[php]
<?php
foreach ($article->getAllResourceVersions() as $resourceVersion)
{
echo sprintf("'%s', Version %d, updated by %s on %s (%s)\n",
$resourceVersion->getTitle(),
$resourceVersion->getNumber(),
$resourceVersion->getCreatedBy(),
$resourceVersion->getCreatedAt(),
$resourceVersion->getComment(),
);
}
Tip: If you have `auto_versioning` set to off and use the manual `addVersion()` process, you can pass the author of the revision and the comment as parameters to the method call, as follows:
[php]
<?php
$article = new Article();
$article->setTitle('Original title');
$article->addVersion('John Doe', 'Article creation');
$article->save();
### Versioning Related objects
If you use the manual `addVersion()`, you can specify that you want related objects to be versioned together with the main object. For instance, imagine an `Article` model with a many-to-one relationship to a `Category` model:
[php]
<?php
$article = new Article();
$article->setTitle('Original title');
$category = new Category();
$category->setName('Category1');
$article->setCategory($category);
// Explicitly ask to include the `Category` object in the versioning process
$article->addVersion('author1', 'comment1', array('Category'));
$article->save();
$article->setTitle('Modified title');
$category = new Category();
$category->setName('Category1');
$article->setCategory($category);
// Explicitly ask to include the `Category` object in the versioning process
$article->addVersion('author2', 'comment2', array('Category'));
$article->save();
$article->toVersion(1);
echo $article->getCategory()->getName(); // 'Category1'
$article->toVersion(2);
echo $article->getCategory()->getName(); // 'Category2'
The same works for one-to-many relationhips, with a trick. For instance, if the `Article` model can have many `Comments`:
[php]
<?php
$article = new Article();
$article->setTitle('Original title');
$comment1 = new Comment();
$comment1->setContent('Comment1');
$article->addComment($comment1);
$comment2 = new Comment();
$comment2->setContent('Comment2');
$article->addComment($comment2);
// Explicitly ask to include the `Comment` objects in the versioning process
$article->addVersion('author1', 'comment1', array('Comments'));
$article->save();
$article->setTitle('Modified title');
$comment1->setContent('Comment1 Modified');
$comment1->save();
$comment2->setContent('Comment2 Modified');
$comment2->save();
// Explicitly ask to include the `Comment` objects in the versioning process
$article->addVersion('author2', 'comment2', array('Comments'));
$article->save();
$comments = $article->toVersion(1)->getComments();
echo $comments[0]->getContent(); // 'Comment1'
echo $comments[1]->getContent(); // 'Comment2'
$comments = $article->toVersion(2)->getComments();
echo $comments[0]->getContent(); // 'Comment1 Modified'
echo $comments[1]->getContent(); // 'Comment2 Modified'
Note: For the one-to-many versioning to work, you need to override the base object `initXXX()` method. In the above example, you must override the `Article::initComments()` method from:
// in BaseArticle.php
public function initComments()
{
if ($this->collComments === null) {
$this->collComments = array();
}
}
to:
// in Article.php
public function initComments($force = false)
{
if ($this->collComments === null || $force) {
$this->collComments = array();
}
}
This modification should not affect the rest of your model.
Alternatively, if you prefer to let the Propel generator modify your `initXXX()` methods automatically for all models, you just need to change one line in your `propel.ini` and rebuild your model:
propel.builder.object.class = plugins.sfPropelVersionableBehaviorPlugin.lib.SfVersionableObjectBuilder
## Public API
### Object API
Enabling the behaviors adds / modifies the following method to the Propel objects :
* `void save()`: Adds a new version to the object version history and increments the `version` property
* `void delete()`: Deletes the object version history
* `void 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).
* `array getAllVersions()`: Returns all versions of the object in an ordered array
* `void addVersion(string $updatedBy, string $comment, array $withObjects)`: Increments the object's version number (without saving it) and creates a new ResourceVersion record. To be used when versionConditionMet() is false
* `ResourceVersion getLastResourceVersion()`: Returns the object's last version object
* `ResourceVersion getCurrentResourceVersion()`: Returns the object's current version object
* `array getAllResourceVersions()`: Returns all version objects in an array
* `void setResourceCreatedBy(string $createdBy)`: Defines the author name for the revision
* `string getResourceCreatedBy()`: Gets the author name for the revision
* `mixed getResourceCreatedAt()`: Gets the creation date for the revision (but you'd better have an `updated_at` column in your model)
* `void setResourceComment(string $comment)`: Defines the comment for the revision
* `string getResourceComment()`: Gets the comment for the revision
### !ResourceVersion API
* `BaseObject getResourceInstance()`: Returns resource instance populated with attributes from the revision
* `int getNumber()`: Returns the version number of the object revision
* `string getCreatedBy()`: Returns the author of the object revision
* `string getTitle()`: Returns the title of the object revision
* `string getComment()`: Returns the comment of the object revision
* `mixed getCreatedAt(string $format)`: Returns the date of the object revision
### sfPropelVersionableBehavior API
* (static) `string setVersionConditionMethod(string $method_name)`: Sets object method used to decide if a new version should be created
* (static) `string getVersionConditionMethod()`: Returns version condition method name
## Roadmap
### 0.4
* Make plugin compatible with sfPropel's i18n capabilities
* Avoid saving unchanged columns and records to savce database space
* Change the calls to `getPrimaryKey` by calls to a [sfPropelActAsRatableBehaviorPlugin](/plugins/sfPropelActAsRatableBehaviorPlugin)-style `getReferenceKey` to allow extending objects with multiple primary keys.
## Changelog
### 2008-03-21 | 0.3.0 beta
* francois: [Break](BC) Added a `title` column to the `resource_version` table and support for resource title
* francois: Added support for related objects versioning (only via `addVersion` for now)
* francois: [Break](BC) Added a `resource_version_id` column to the `resource_version` table
* francois: Fixed error when using 'addVersion' on a new object (primary and foreign keys were not saved)
* francois: Added `getCurrentResourceVersion`, `setResourceCreatedBy`, `getResourceCreatedBy`, `setResourceComment` and `getResourceComment` methods to the public API
* francois: Fixed error when trying to add a version to an unsaved object
* francois: Fixed error when using a version column different than 'version'
* francois: [Break](BC) Added `comment`, `created_by` and `created_at` columns to the `ResourceVersion` class.
* francois: Added `addVersion` method and refactored the behavior to keep D.R.Y.
* francois: More explicit documentation on installation
* francois: Added a few unit tests
* francois: Added a `getAllVersions` method returning an array of origin objects in a single query
* francois: [Break](BC) Renamed `getAllVersions` to `getAllResourceVersions`
* francois: [Break](BC) Renamed `getLastVersions` to `getLastResourceVersion`
* francois: [Break](BC) Replaced uuid by a simple composite key class name + id
### 2008-02-08 | 0.2.3 alpha
* francois: Fixed error when using a version column different than 'version'
### 2008-02-06 | 0.2.2 alpha
* francois: Made the doc more explicit about multiple models (fixes #1820)
* francois: Removed need for hardcoded foreign key (fixes #1562)
* francois: Switched plugin schema to YAML
* francois: Made the unit tests more adaptable
### 2007-16-03 | 0.2.1 alpha
* #1563 : does not create a version if YourClass::versionConditionMet() is not found (madman)
* #1564 : crashes while creating a new version if no prior version exists (madman)
* Syntax highlighting in README
* added `sfPropelVersionableBehavior::getVersionConditionMethod()` and `sfPropelVersionableBehavior::setVersionConditionMethod()` methods
* enhanced inner management of conditional versioning
* updated unit tests and docs accordingly
### 2007-02-17 | 0.2.0 alpha
* made version number management more reliable
* new `getLastVersion()` method
* implemented conditional versioning
* updated docs and unit tests accordingly
### 2007-02-17 | 0.1.0 alpha
Initial public release.