John Main Logo

John Main

Code. Design. Hosting. Maintenance.

02
Jan '18

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.