laravel-versionable: revision history and revert for Eloquent models
⏱️Make Laravel model versionable
At a glance
- What is it?
- laravel-versionable is a Laravel package that gives an Eloquent model a version history, records only the attributes that changed, and lets you revert to any stored version with one call. It is deliberately small, it stores versions in a table you can swap out, and the diff between two versions comes from a separate library rather than from the package itself.
- Who is it for?
- Adopt laravel-versionable if you run Laravel and need an auditable history of edits to a model, for example for a content or configuration table where a mistaken update has to be undoable, and if you are comfortable with a package that stores versions in the same database.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 17 days ago.
- What is it written in?
- Mainly PHP, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 24, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A trait and a list of attributes
The package is described as a minimalist way to make a model support version history, and it is very simple to revert to the specified version.
The mechanism is a trait. You add the Versionable trait to a model and list the attributes you want versioned, in a $versionable property. The alternative is a blacklist, a $dontVersionable property, and the documentation shows it commented out with created_at and updated_at as the typical example.
use Overtrue\LaravelVersionable\Versionable;
class Post extends Model
{
use Versionable;
/**
* Versionable attributes
*
* @var array
*/
protected $versionable = ['title', 'content'];
// Or use a blacklist
//protected $dontVersionable = ['created_at', 'updated_at'];
}Versions are created when the versionable model is saved, which means the history follows the Eloquent lifecycle rather than requiring a call of your own. The two example lines that follow show the whole point of the package:
$post = Post::create(['title' => 'version1', 'content' => 'version1 content']);
$post->update(['title' => 'version2']);A create and an update produce two versions, and the second one is what the model looks like now.
The requirement list is short: PHP 8.1 or higher, and laravel/framework 9.0 or higher. The current release line is 6.x, and the default branch in the repository is 5.x with the recent releases at v6.0.1 from 2026-10-07 and v6.0.0 from 2026-03-21. The package is MIT licensed.
Reading versions back, by position or by time
The read side is symmetric with the write side, and it offers several ways to ask the same question.
There is a versions relation for all of them, and named accessors for the ones you usually want. The latest version is available as latestVersion or lastVersion, and the first as versions->first() or firstVersion. The relation itself is available on the model.
$post->versions; // all versions
$post->latestVersion; // latest version
// or
$post->lastVersion;
$post->versions->first(); // first version
// or
$post->firstVersion;
$post->versionAt('2022-10-06 12:00:00'); // get version from a specific time
// or
$post->versionAt(\Carbon\Carbon::create(2022, 10, 6, 12));The versionAt lookup takes either a timestamp string or a Carbon instance, which is a useful detail because the package leans on Carbon elsewhere. Fetching a version by the time it existed is the API you want when someone asks what the record looked like when they complained, and it is the API that would be awkward to build on top of a package that only stored integer sequence numbers.
The naming is a little inconsistent, with both lastVersion and latestVersion meaning the same thing, and both firstVersion and versions->first() meaning the same thing. That is a minor thing, but it is the kind of thing you notice when you are reading the API for the first time.
Reverting, and reverting without saving
Reverting is a single call, and there are two spellings of it. You can take a version object and call revert on it, or you can call revertToVersion with a number on the model.
$post->getVersion(3)->revert();
// or
$post->revertToVersion(3);There is also a version that does not write to the database, which is the one to reach for when you want to show someone what a previous state looked like before committing to it:
$version = $post->versions()->first();
$post = $version->revertWithoutSaving();The distinction matters more than it looks. A plain revert writes, and in a request cycle that means the change is live immediately. revertWithoutSaving populates the model in memory, so you can inspect it, diff it against the current state or pass it to a confirmation screen, and nothing is persisted until you save.
Version removal comes in two flavours, soft and force. The soft ones take an id or an array of ids, and there is a method to remove all of them.
// soft delete
$post->removeVersion($versionId = 1);
$post->removeVersions($versionIds = [1, 2, 3]);
$post->removeAllVersions();
// force delete
$post->forceRemoveVersion($versionId = 1);
$post->forceRemoveVersions($versionIds = [1, 2, 3]);
$post->forceRemoveAllVersions();A soft delete keeps the row and marks it, and restoreTrashedVersion brings a deleted version back by id, which means the removal and the restore are both non-destructive operations and the version history can be audited after the fact.
Diff strategy versus snapshot strategy
The package exposes a version strategy property, $versionStrategy, with two values, and the choice determines what a stored version contains.
The DIFF strategy stores only the attributes that changed, and it is the default. The SNAPSHOT strategy stores all the versionable attribute values.
This is the single most consequential design decision in the package, so it is worth spelling out what each one costs. A diff row is small, because it holds only what moved since the previous version, so a table with a thousand edits to a wide record costs far less than a thousand full copies. A snapshot row is a complete copy, which is simple to reason about and expensive to store, and it makes a revert trivial because the data is already whole.
The subtlety is that a diff row is not a self-contained record. Reverting to version three with diff rows means applying that version and everything after it, or reconstructing the state from the sequence. With snapshots, revert is a write of one row's worth of attributes.
A related feature is that only changed attributes are recorded at all, which is listed separately in the feature list from the strategy choice. That is the diff strategy doing its job: a save that changes nothing does not create a version, so the history contains edits rather than saves.
There is also a version limit. The package can keep a specified number of versions, which bounds the storage of either strategy, and it is the setting to think about before enabling this on a table with a lot of traffic.
Diffing two versions with a borrowed renderer
The package can show the difference between two versions, and the implementation is a thin wrapper over a separate library rather than a diff engine of its own.
$diff = $post->getVersion(1)->diff($post->getVersion(2));The result is an object of type Overtrue\LaravelVersionable\Diff, and it is based on jfcherng/php-diff. The documentation then shows that the diff renders to many formats, and that the result is an array keyed by attribute name:
[
$attribute1 => $diffOfAttribute1,
$attribute2 => $diffOfAttribute2,
...
$attributeN => $diffOfAttributeN,
]The toArray method gives the structured form:
$diff->toArray();
//
[
"name" => [
"old" => "John",
"new" => "Doe",
],
"age" => [
"old" => 25,
"new" => 26,
],
]And the other output methods are listed with their shared signature:
toArray(array $differOptions = [], array $renderOptions = [], bool $stripTags = false): array
toText(array $differOptions = [], array $renderOptions = [], bool $stripTags = false): array
toJsonText(array $differOptions = [], array $renderOptions = [], bool $stripTags = false): array
toContextText(array $differOptions = [], array $renderOptions = [], bool $stripTags = false): array
toHtml(array $differOptions = [], array $renderOptions = [], bool $stripTags = false): array
toInlineHtml(array $differOptions = [], array $renderOptions = [], bool $stripTags = false): array
toJsonHtml(array $differOptions = [], array $renderOptions = [], bool $stripTags = false): array
toSideBySideHtml(array $differOptions = [], array $renderOptions = [], bool $stripTags = false): arrayThe differ and render options are optional and are passed through to the php-diff documentation, and the stripTags flag removes HTML tags from the diff, which is helpful when you do not want to show them.
Basing the diff on a separate library is a sensible division of labour. A diff between two attribute values is a small, well-understood problem, and reusing a library that already renders to HTML, plain text, JSON and side-by-side HTML means this package does not have to maintain a renderer. It also means the diff output format is not yours to control, and the side-by-side HTML in particular is designed for a human reading a comparison rather than for embedding in an application view.
Swapping the version table and switching versioning off
Two extension points are documented, and they cover the situations where the defaults are wrong for your application.
The first is the version model. You can define a $versionModel property on a model using the trait, and that model is the table used for versions. The documentation is strict about the requirement: the model must extend the class \Overtrue\LaravelVersionable\Version.
<?php
class PostVersion extends \Overtrue\LaravelVersionable\Version
{
//
}And the attribute on the model is typed:
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Overtrue\LaravelVersionable\Versionable;
class Post extends Model
{
use Versionable;
public string $versionModel = PostVersion::class;
}That is what you need when the version rows should carry extra columns, or should live in a table with a different naming convention, or should be scoped per tenant. The requirement to extend the package's Version class is what keeps the relationship between a model and its versions working.
The second extension point is a way to turn versioning off for a block of code. The withoutVersion helper takes a callback and runs it with versioning suspended, for both creation and update:
// create
Post::withoutVersion(function () use (&$post) {
Post::create(['title' => 'version1', 'content' => 'version1 content']);
});
// update
Post::withoutVersion(function () use ($post) {
$post->update(['title' => 'updated']);
});This is the escape hatch for bulk imports, seeding and migrations, where producing a version per row would be noise. It is also a trap if you use it for a normal update, since the change is then invisible to the history.
The display side is a separate project. The integrations section lists a Filament package, mansoorkhan96/filament-versionable, described as a way to manage revisions of Eloquent models in Filament. That tells you where the visual review UI is expected to come from, and it also tells you that laravel-versionable on its own gives you an API and a diff, not a screen.
Where laravel-versionable is the wrong choice
The first limitation is that it versions state, not events. Every version row is a picture of the model at a moment, and the history is a sequence of pictures. If what you need is to know who did what and why, or to replay a process, a picture is the wrong shape, and an event log or an activity package is the right one. The package is built for a model whose current value can be reconstructed from a previous value, and it is at its best exactly there.
The second limitation is the storage cost of that shape, and it interacts badly with the diff strategy. A diff row holds only the changed attributes, which is cheap per row, but a revert to an early version is not a single read. With snapshots, a revert is a write of one row, and the table grows with the width of the model. A wide model on a hot table is the case where the version limit has to be aggressive, and an aggressive limit is a short audit trail.
The third limitation is that the package is bound to Eloquent and to the Laravel version range. The requirement is laravel/framework 9.0 and higher with PHP 8.1, and the 6.x release line is current, but the coupling to Eloquent's save lifecycle means the versioning fires on model events rather than on your intent. A save that touches nothing meaningful still goes through the model, and if your application updates rows outside Eloquent, those changes are not versioned.
The real alternative is a dedicated versioning or activity package, and the difference in approach is the difference between state and event. An event-oriented package records what happened, so it can answer who did it and in what order, and it stores a compact description of each action rather than a copy of the record. laravel-versionable stores copies, so it can answer what the record looked like at any point and revert to it exactly, with the audit trail as a by-product. If the question your team asks is what did this row look like before the change, this package answers it directly. If the question is who changed this and why, it does not, and the diff view is showing you a comparison of values rather than a record of actions.
Editorial conclusion
Adopt laravel-versionable if you run Laravel and need an auditable history of edits to a model, for example for a content or configuration table where a mistaken update has to be undoable, and if you are comfortable with a package that stores versions in the same database. Do not adopt it as a general event-sourcing or activity-feed system, since it records state snapshots of a model rather than a stream of domain events, and do not adopt it expecting a full revision UI, since the display side is a separate project. Before you commit, decide between the diff and snapshot version strategies, because the diff strategy stores only changed attributes and a revert has to reconstruct the rest, and check the version retention setting, since the package keeps a specified number of versions and a truncated history is a truncated audit trail.
Frequently asked questions
How do I add version history to a Laravel model?
You add the Overtrue\LaravelVersionable\Versionable trait to the model and list the attributes to version in a $versionable property, or use the $dontVersionable property as a blacklist. Versions are created when the versionable model is saved.
How do I install laravel-versionable?
Require the package with composer require overtrue/laravel-versionable -vvv, then publish the config file and migrations with php artisan vendor:publish --provider="Overtrue\LaravelVersionable\ServiceProvider", and run php artisan migrate to create the database migration. The package requires PHP 8.1 or higher and laravel/framework 9.0 or higher.
What is the difference between the DIFF and SNAPSHOT version strategies?
With the DIFF strategy the stored version contains only the attributes that changed, and that is the default. With the SNAPSHOT strategy the version contains all the versionable attribute values. You select between them through the $versionStrategy property on the model.
How do I revert a Laravel model to an earlier version?
Either call revert on a version object with $post->getVersion(3)->revert(), or call $post->revertToVersion(3) on the model. There is also revertWithoutSaving, which populates the model in memory without writing it to the database.
Can I use a custom table for version records?
Yes. You define a $versionModel property on the model using the trait, and that model is used for the version records. The documentation requires that the model extend the class \Overtrue\LaravelVersionable\Version, and the example defines a PostVersion class and assigns public string $versionModel = PostVersion::class on the model.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/overtrue-laravel-versionable)Was this useful?