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.