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.