thunderwolf/versionable

Versionable for the Laravel Eloquent

1.0.0 2022-03-25 17:44 UTC

This package is auto-updated.

Last update: 2024-04-25 21:57:05 UTC


README

https://geekflare.com/laravel-eloquent-model-relationship/

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

To work it requires Versionable trait in your Model with a configuration which in the most simple configuration just looks like this:

    public static function versionable(): array
    {
        return [
            'version_model' => OurModelVersion::class
        ];
    }

and we need a special Model which will be used to store versions which will require VersionableVersion trait with a configuration which in the most simple configuration just looks like this:

    public static function versionableVersion(): array
    {
        return [
            'version_columns' => ['columnA', 'columnB', 'columnN'],
            'versionable_model' => OurModel::class
        ];
    }

Basic Usage

The most basic way to use this package is to Create Model with the Versionable trait in use like this:

<?php

use Illuminate\Database\Eloquent\Model;
use Thunderwolf\EloquentVersionable\Versionable;

class Book extends Model
{
    use Versionable;

    protected $table = 'book';

    protected $fillable = ['title'];

    public $timestamps = false;

    public static function versionable(): array
    {
        return [
            'version_model' => BookVersion::class
        ];
    }
}

and Create Version Model with the VersionableVersion trait in use like this:

<?php

use Illuminate\Database\Eloquent\Model;
use Thunderwolf\EloquentVersionable\VersionableVersion;

class BookVersion extends Model
{
    use VersionableVersion;

    protected $table = 'book_version';

    protected $fillable = ['title'];  // Must be the same as we have in the `version_columns`

    public $timestamps = false;
    public $incrementing = false;

    public static function versionableVersion(): array
    {
        return [
            'version_columns' => ['title'],
            'versionable_model' => Book::class
        ];
    }
}

After registering VersionableServiceProvider you can also use Blueprints to create tables with a use of createVersionable and createVersionableVersion helpers method similar to this:

$schema->create('book', function (Blueprint $table1) {
    $table1->increments('id');
    $table1->string('title');
    $table1->createVersionable(['version_model' => BookVersion::class]);
});

$schema->create('book_version', function (Blueprint $table2) {
    $table2->unsignedInteger('title');
    $table2->createVersionableVersion(['versionable_model' => Book::class, 'version_columns' => ['title']]);
});

In similar way you will be working with migrations.

The model now has one new column, version_column, 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();

$book->setAttribute('title', 'War and Peas');
$book->save();
echo $book->getVersion(); // 1

$book->setAttribute('title', 'War and Peace');
$book->save();
echo $book->getVersion(); // 2

$book->toVersion(1);
echo $book->getTitle(); // 'War and Peas'
$book->save();
echo $book->getVersion(); // 3

$diff = $book->compareVersions(1, 2);
// print_r($diff);
// array(
//   'Title' => array(1 => 'War and Peas', 2 => 'War and Pace'),
// );

$id = $book->getKey();

// deleting an object also deletes all its versions
$book->delete();

$obj = Book::query()->find($id);
$this->assertNull($obj);

$collection = BookVersion::query()->where('id',$id)->get();
$collection->isEmpty(); // true

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 array.

For example BookDetails model:

<?php

use Illuminate\Database\Eloquent\Model;
use Thunderwolf\EloquentVersionable\Versionable;

class BookDetails extends Model
{
    use Versionable;

    protected $table = 'book-details';

    protected $fillable = ['title'];

    public $timestamps = false;

    public static function versionable(): array
    {
        return [
            'version_model' => BookDetailsVersion::class,
            'log_created_at' => true,
            'log_created_by' => true,
            'log_comment' => true,
        ];
    }
}

and BookDetailsVersion model:

<?php

use Illuminate\Database\Eloquent\Model;
use Thunderwolf\EloquentVersionable\VersionableVersion;

class BookDetailsVersion extends Model
{
    use VersionableVersion;

    protected $table = 'book-details-version';

    protected $fillable = ['title'];  // Must be the same as we have in the `version_columns`

    public $timestamps = false;
    public $incrementing = false;

    public static function versionableVersion(): array
    {
        return [
            'version_columns' => ['title'],
            'versionable_model' => BookDetails::class,
            'log_created_at' => true,
            'log_created_by' => true,
            'log_comment' => true,
        ];
    }
}

to create those tables:

$schema->create('book-details', function (Blueprint $table3) {
    $table3->increments('id');
    $table3->string('title');
    $table3->createVersionable([
        'version_model' => BookDetailsVersion::class,
        'log_created_at' => true,
        'log_created_by' => true,
        'log_comment' => true,
    ]);
});

$schema->create('book-details-version', function (Blueprint $table4) {
    $table4->string('title');
    $table4->createVersionableVersion([
        'versionable_model' => BookDetails::class, 'version_columns' => ['title'],
        'log_created_at' => true,
        'log_created_by' => true,
        'log_comment' => true,
    ]);
});

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 BookDetails();
$book->setAttribute('title', 'War and Peas');
$book->setVersionCreatedBy('John Doe');
$book->setVersionComment('Book creation');
$book->save();

$book->setAttribute('title', '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(); // 'YYYY-MM-DD HH:MM:SS'

// 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 YYYY-MM-DD HH:MM:SS (Book Creation)
// 'War and Peace', Version 2, updated by John Doe on YYYY-MM-DD HH:MM:SS (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.

For example BookVersioningNecessary model:

<?php

use Illuminate\Database\Eloquent\Model;
use Thunderwolf\EloquentVersionable\Versionable;

class BookVersioningNecessary extends Model
{
    use Versionable {
        isVersioningNecessary as protected traitIsVersioningNecessary;
    }

    protected $table = 'book-versioning-necessary';

    protected $fillable = ['title', 'isbn'];

    public $timestamps = false;

    public function isVersioningNecessary(): bool
    {
        return $this->getAttribute('isbn') !== null && $this->traitIsVersioningNecessary();
    }

    public static function versionable(): array
    {
        return [
            'version_model' => BookVersioningNecessaryVersion::class
        ];
    }
}

and BookVersioningNecessaryVersion model:

<?php

use Illuminate\Database\Eloquent\Model;
use Thunderwolf\EloquentVersionable\VersionableVersion;

class BookVersioningNecessaryVersion extends Model
{
    use VersionableVersion;

    protected $table = 'book-versioning-necessary-version';

    protected $fillable = ['title', 'isbn'];  // Must be the same as we have in the `version_columns`

    public $timestamps = false;
    public $incrementing = false;

    public static function versionableVersion(): array
    {
        return [
            'version_columns' => ['title'],
            'versionable_model' => BookVersioningNecessary::class
        ];
    }
}

to create those tables:

$schema->create('book-versioning-necessary', function (Blueprint $table5) {
    $table5->increments('id');
    $table5->string('title');
    $table5->string('isbn')->nullable();
    $table5->createVersionable(['version_model' => BookVersioningNecessaryVersion::class]);
});

$schema->create('book-versioning-necessary-version', function (Blueprint $table6) {
    $table6->string('title');
    $table6->string('isbn')->nullable();
    $table6->createVersionableVersion(['versionable_model' => BookVersioningNecessary::class, 'version_columns' => ['title', 'isbn']]);
});

and we can use this like here:

<?php
$book = new BookVersioningNecessary();
$book->setAttribute('title', 'War and Peas');
$book->save(); // book is saved, no new version is created
$book->setAttribute('isbn', '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
Book::disableVersioning();
$book = new Book();
$book->setAttribute('title', '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 re-enable versioning using the Query static method enableVersioning()
Book::enableVersioning();

Parameters

In case of the Model which will be versioned we have this additional configuration parameters which were not mentioned upper:

  • version_column which by default is set to be version, you can choose a name of this column just by setting it with this key,
  • version_created_at_column which by default is set to be version_created_at, you must log_created_at to true to use it,
  • version_created_by_column which by default is set to be version_created_by, you must log_created_by to true to use it,
  • version_comment_column which by default is set to be version_comment, you must log_comment to true to use it.

In case of the Version Model we can set additional configuration parameters which were not mentioned upper:

  • versionable_model_foreign_key which by default is set to be id.

Public API

// Adds a new version to the object version history and increments the version property
public function save(array $options = []): bool

// Deletes the object and version history
public function delete(): bool

// Checks whether a new version needs to be saved
public function isVersioningNecessary(): bool

// 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).
public function toVersion(int $versionNumber): self

// Queries the database for the highest version number recorded for this object
public function getLastVersionNumber(): int

// Returns true if the current object is the last available version
public function isLastVersion(): bool

// Creates a new Version record and saves it. To be used when `isVersioningNecessary()` is false. Beware that it does not take care of incrementing the version number of the main object, and that the main object must be saved prior to calling this method.
public function addVersion(): Model

// Returns all Version objects related to the main object in a collection
public function getAllVersions(): Collection

// Returns a given version object
public function getOneVersion(int $versionNumber): ?Model

// Returns an array of differences showing which parts of a resource changed between two versions
public function compareVersions(int $fromVersionNumber, int $toVersionNumber, string $keys = 'columns', array $ignoredColumns = []): array

// Populates an ActiveRecord object based on a Version object
public function populateFromVersion(Model $version, array &$loadedObjects = []): self

// Defines the author name for the revision
public function setVersionCreatedBy($user): void

// Gets the author name for the revision
public function getVersionCreatedBy()

// Gets the creation date for the revision (the behavior takes care of setting it)
public function getVersionCreatedAt(): Carbon

// Defines the comment for the revision
public function setVersionComment(?string $comment): void

// Gets the comment for the revision
public function getVersionComment(): ?string

Static methods

// Enables versioning for all instances of the related ActiveRecord class
public static function enableVersioning(): void

// Disables versioning for all instances of the related ActiveRecord class
public static function disableVersioning(): void

// Checks whether the versionnig is enabled
public static function isVersioningEnabled(): bool