sfPropelVersionableBehaviorPlugin - 0.4.0

Propel versionable behavior

You are currently browsing
the website for symfony 1

Visit the Symfony2 website


« Back to the Plugins Home

Signin


Forgot your password?
Create an account

Tools

Stats

advanced search
Information Readme Releases Changelog Contribute
Show source | Show as Markdown

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] 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]

    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
 
$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

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
 
// 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
 
sfPropelBehavior::add('Article', array('versionable' => array('columns' => $columns_map, 'conditional' => 'myMethod')));

It is possible to change this method at runtime :

<?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
 
$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:

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:

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
 
$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();

Retrieving a resource version history

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
 
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
 
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
 
$article = new Article();
$article->setTitle('Original title');
$article->addVersion('John Doe', 'Article creation');
$article->save();

Versioning Related objects

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:

// Explicitly ask to include the `Category` object in the versioning process
sfPropelBehavior::add('Article', array('versionable' => array(
  'with'   => array('Category')
)));
 
$article = new Article();
$article->setTitle('Original title');
$category = new Category();
$category->setName('Category1');
$article->setCategory($category);
$article->save();
 
$article->setTitle('Modified title');
$category = new Category();
$category->setName('Category1');
$article->setCategory($category);
$article->save();
 
$article->toVersion(1);
echo $article->getCategory()->getName();    // 'Category1'
$article->toVersion(2);
echo $article->getCategory()->getName();    // 'Category2'

You can choose to include different related objects if you use the addVersion() method. Specify which objects to include in the versioning process in an array, and use that array as a third argument of addVersion(). For instance, if you didn't add any with parameter during the behavior declaration, you can still save the related Category objects in the example above by calling, before each save():

...
$article->addVersion('author1', 'comment1', array('Category'));
$article->save();
...
$article->addVersion('author2', 'comment2', array('Category'));
$article->save();

The same works for one-to-many relationhips, with a trick. For instance, if the Article model can have many Comments:

<?php
// Explicitly ask to include the `Comment` objects in the versioning process
// Note that `Comment` is declared as a plural
sfPropelBehavior::add('Article', array('versionable' => array(
  'with'   => array('Comments')
)));
 
$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);
$article->save();
 
$article->setTitle('Modified title');
$comment1->setContent('Comment1 Modified');
$comment1->save();
$comment2->setContent('Comment2 Modified');
$comment2->save();
$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

Note: If you use the sfPropelAlternativeSchemaPlugin plugin, you don't need to change the Propel object builder, since the alternative schema's builder includes this modification.

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).
  • boolean isLastVersion(): Returns true if the current object is the last available 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
  • ResourceVersion getResourceVersion(integer $version_number): Returns the object's numbered 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.5

  • Make plugin compatible with sfPropel's i18n capabilities
  • Change the calls to getPrimaryKey by calls to a sfPropelActAsRatableBehaviorPlugin-style getReferenceKey to allow extending objects with multiple primary keys.

Changelog

2008-04-05 | Trunk

2008-04-05 | 0.4.0 beta

  • francois: Made incremental storage rely on a real version comparison, rather than the array of modified columns. Fixes modified columns not being saved when using toVersion.
  • francois: Added the ability to declare related objects to save at behavior declaration
  • francois: Fixed ResourceVersion::getResourceInstance() creates new objects and saving these objects creates a new row in the resource table (#3229)
  • francois: Added isLastVersion() method
  • francois: Avoid saving unchanged records to save database space (refs #3150)
  • francois: Added ResourceAttributeVersion::getResourceVersions() method
  • francois: Added ResourceVersion::getResourceAttributeVersions() method
  • francois: Avoid saving unchanged columns to save database space
  • francois: Break Added a resource_attribute_version_hash table, now middle table between versions and attributes

2008-03-21 | 0.3.0 beta

  • francois: Break 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 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 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 Renamed getAllVersions to getAllResourceVersions
  • francois: Break Renamed getLastVersions to getLastResourceVersion
  • francois: Break 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.