 
 
 
 
 
 , (*1)
, (*1)
Version
A CakePHP 4.x plugin that facilitates versioned database entities, (*2)
Installation
Add the following lines to your application's composer.json:, (*3)
"require": {
    "josegonzalez/cakephp-version": "dev-master"
}
followed by the command:, (*4)
composer update, (*5)
Or run the following command directly without changing your composer.json:, (*6)
composer require josegonzalez/cakephp-version:dev-master, (*7)
Usage
In your app's config/bootstrap.php add:, (*8)
Plugin::load('Josegonzalez/Version', ['bootstrap' => true]);
Usage
Run the following schema migration:, (*9)
CREATE TABLE `version` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `version_id` int(11) DEFAULT NULL,
    `model` varchar(255) NOT NULL,
    `foreign_key` int(10) NOT NULL,
    `field` varchar(255) NOT NULL,
    `content` text NULL,
    `created` datetime NOT NULL,
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  
  Note that the content field must be nullable if you want to be able to version any nullable fields in your application., (*10)
  
  You may optionally add a version_id field of type integer to the table which is being versioned. This will store the latest version number of a given page., (*11)
If you wish to create the table using cakephp/migrations then you will need to use a migration that looks something like this:, (*12)
<?php
use Phinx\Migration\AbstractMigration;
class CreateVersions extends AbstractMigration
{
    public function change()
    {
        $this->table('version')
             ->addColumn('version_id', 'integer', ['null' => true])
             ->addColumn('model', 'string')
             ->addColumn('foreign_key', 'integer')
             ->addColumn('field', 'string')
             ->addColumn('content', 'text', ['null' => true])
             ->addColumn('created', 'datetime')
             ->create();
    }
}
Add the following line to your entities:, (*13)
use \Josegonzalez\Version\Model\Behavior\Version\VersionTrait;
And then include the trait in the entity class:, (*14)
class PostEntity extends Entity {
    use VersionTrait;
}
Attach the behavior in the models you want with:, (*15)
public function initialize(array $config) {
    $this->addBehavior('Josegonzalez/Version.Version');
}
Whenever an entity is persisted - whether via insert or update - that entity is also persisted to the version table. You can access a given revision by executing the following code:, (*16)
// Will contain a generic `Entity` populated with data from the specified version.
$version = $entity->version(1);
You can optionally retrieve all the versions:, (*17)
$versions = $entity->versions();
cakephp-version dispatches an event Model.Version.beforeSave which you can optionally handle to attach additional meta-data about the version., (*18)
Add the necessary additional fields to your migration, for example:, (*19)
CREATE TABLE `version` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `version_id` int(11) DEFAULT NULL,
    `model` varchar(255) NOT NULL,
    `foreign_key` int(10) NOT NULL,
    `field` varchar(255) NOT NULL,
    `content` text,
    `created` datetime NOT NULL,
    `custom_field1` varchar(255) NOT NULL, /* column to store our metadata */
    `custom_field2` varchar(255) NOT NULL, /* column to store our metadata */
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Then define an event listener to handle the event and pass in additional metadata, for example:, (*20)
use Cake\Event\Event;
use Cake\Event\EventListenerInterface;
class VersionListener implements EventListenerInterface {
    public function implementedEvents() {
        return array(
            'Model.Version.beforeSave' => 'insertAdditionalData',
        );
    }
    public function insertAdditionalData(Event $event) {
        return [
            'custom_field1' => 'foo',
            'custom_field2' => 'bar'
        ];
    }
}
Your event listener can then be attached in your project, for example:, (*21)
use App\Event\VersionListener;
use Cake\Event\EventManager;
$VersionListener = new VersionListener();
EventManager::instance()->on($VersionListener);
Note that handling this event also allows you to modify/overwrite values generated by the plugin.
This can provide useful functionality, but ensure that if your event listener returns array keys called
version_id, model, foreign_key, field, content or created that this is the intended behavior., (*22)
To store the user_id as additional meta data is easiest in combination with Muffin/Footprint.
The above insertAdditionalData() method could then look like this:, (*23)
    /**
     * @param \Cake\Event\Event $event
     *
     * @return array
     */
    public function insertAdditionalData(Event $event)
    {
        $data = [
            ...
        ];
        if ($event->data('_footprint')) {
            $user = $event->data('_footprint');
            $data += [
                'user_id' => $user->id,
            ];
        }
        return $data;
    }
Any controller with the FootprintAwareTrait used will then provide the _footprint data into the model layer for this event callback to use., (*24)
Bake Integration
If you load the plugin using 'bootstrap' => true, this plugin can be used to autodetect usage via the properly named database table. To do so, simply create a table with the version schema above named after the table you'd like to revision plus the suffix _versions. For instance, to version the following table:, (*25)
CREATE TABLE `posts` (
    `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
    `category_id` int(11) DEFAULT NULL,
    `user_id` int(11) DEFAULT NULL,
    `status` varchar(255) NOT NULL DEFAULT 'published',
    `visibility` varchar(255) NOT NULL DEFAULT 'public',
    `title` varchar(255) NOT NULL DEFAULT '',
    `route` varchar(255) DEFAULT NULL,
    `content` text,
    `published_date` datetime DEFAULT NULL,
    `created` datetime NOT NULL,
    `modified` datetime NOT NULL,
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Create the following table:, (*26)
CREATE TABLE `posts_versions` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `version_id` int(11) NOT NULL,
    `model` varchar(255) NOT NULL,
    `foreign_key` int(11) NOT NULL,
    `field` varchar(255) NOT NULL,
    `content` text,
    `created` datetime NOT NULL,
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
You can create a migration for this with the following bake command:, (*27)
bin/cake bake migration create_posts_versions version_id:integer model foreign_key:integer field content:text created
  
  You'll also want to set the content field in this migration to nullable, otherwise you won't be able to version fields that can be nulled., (*28)
To track the current version in the posts table, you can create a migration to add the version_id field to the table:, (*29)
bin/cake bake migration add_version_id_to_posts version_id:integer
Configuration
There are five behavior configurations that may be used:, (*30)
- 
versionTable: (Default:version) The name of the table to be used to store versioned data. It may be useful to use a different table when versioning multiple types of entities.
- 
versionField: (Default:version_id) The name of the field in the versioned table that will store the current version. If missing, the plugin will continue to work as normal.
- 
additionalVersionFields: (Default['created']) The additional or custom fields of the versioned table to be exposed as well. By default prefixed withversion_, e.g.'version_user_id'for'user_id'.
- 
referenceName: (Default: db table name) Discriminator used to identify records in the version table.
- 
onlyDirty: (Default: false) Set to true to version only dirty properties.