John Main Logo

John Main

Code. Design. Hosting. Maintenance.

02
Jan '18

Record Versioning in Laravel

I’ve tried various different mechanisms for versioning records over the years, but recently I’ve been tasked with creating a versioning solution that is unobtrusive, will bolt-on for an existing Laravel site, and which doesn’t require any structural changes to existing tables.

The way I decided to approach this was via an ‘Archive’ table (with associated model) with a polymorphic relation to the parent record, whatever it be, and a trait that could be applied to any other models and would automatically create a versioned copy on of the parent whenever it was saved. Broadly speaking it looks like this:

Migration

Schema::create('archives', function (Blueprint $table) {
$table->increments('id');
$table->integer('parent_id')->unsigned()->index();
$table->string('parent_type');
$table->longText('data');
$table->timestamps();
$table->index(['parent_id', 'parent_type']);
});

Model

class Archive extends \Eloquent {
protected $fillable = ['parent_id', 'parent_type', 'data'];
/**
* Parent relationship
*/
public function parent() {
return $this->morphTo();
}
/**
* Deserialise / hydrate
* @return \Eloquent
*/
public function deserialise() {
$deserialised = new with($this->parent_type);
$deserialised->fill(unserialize($this->data));
return $deserialised;
}
}

In practice I would usually also store additional meta-data in the archive record, relating to the user that did the save, and any contextual information. Of course we’d want timestamps here too.

Trait

trait Archivable
{
/**
* Custom boot method
*/
public static function bootArchivable()
{
/*
* Trigger on update (nothing to archive on initial create)
*/
static::updating(function ($record) {
$record->archive();
});
/*
* Trigger on delete
*/
static::deleting(function ($record) {
$record->clearArchives();
});
}
/**
* Archive relationship
*/
public function archives() {
return $this->morphMany('\App\Model\Archive', 'parent')->orderBy('id', 'desc');
}
/**
* Clear all archives
*/
public function clearArchives() {
$this->archives()->delete();
}
/**
* Archive
* @return \App\Model\Archive
*/
public function archive() {
return $this->archives()->create([
'data' => serialize($this->getOriginal())
]);
}
/**
* Get previous version
* @return \Eloquent
*/
public function previousVersion() {
if($archive = $this->archives->first()) {
return $archive->deserialise();
}
}
/**
* Get all archives hydrated
* @return Collection
*/
public function allArchives() {
return $this->archives->map(function ($archive) {
return $archive->deserialise();
});
}
}

So the entire parent record is serialised into the ‘data’ field of a new ‘Archive’ record each time the parent is updated. I’ve shown example accessors for previous version(s) with a deserialise/hydrate function to recreate the parent record from an archive.

Leave a Reply