PHPackages                             testmonitor/eloquent-revisable - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. testmonitor/eloquent-revisable

ActiveLibrary[Database &amp; ORM](/categories/database)

testmonitor/eloquent-revisable
==============================

A Laravel package to track revisions of Eloquent models, allowing you to easily view and restore previous versions of your data.

036—10%[1 PRs](https://github.com/testmonitor/eloquent-revisable/pulls)PHP

Since May 19Pushed 2w agoCompare

[ Source](https://github.com/testmonitor/eloquent-revisable)[ Packagist](https://packagist.org/packages/testmonitor/eloquent-revisable)[ RSS](/packages/testmonitor-eloquent-revisable/feed)WikiDiscussions main Synced 3w ago

READMEChangelogDependenciesVersions (6)Used By (0)

Eloquent Revisable
==================

[](#eloquent-revisable)

[![Latest Stable Version](https://camo.githubusercontent.com/78b4958d7b4d75d2460984551e184797a2d91d227440edc515036b87a6ad31f6/68747470733a2f2f706f7365722e707567782e6f72672f746573746d6f6e69746f722f656c6f7175656e742d726576697361626c652f762f737461626c65)](https://packagist.org/packages/testmonitor/eloquent-revisable)[![CircleCI](https://camo.githubusercontent.com/1d89152be81a9b55c24275203ab6e9f4a153fbc871b5b0410e7abb7e2b05b823/68747470733a2f2f696d672e736869656c64732e696f2f636972636c6563692f70726f6a6563742f6769746875622f746573746d6f6e69746f722f656c6f7175656e742d726576697361626c652e737667)](https://circleci.com/gh/testmonitor/eloquent-revisable)[![StyleCI](https://camo.githubusercontent.com/bcdc57ecf72d0deb863bcc49902d94c75760a82774c61a1db1c55b3a01d0cb55/68747470733a2f2f7374796c6563692e696f2f7265706f732f313139323036363331352f736869656c64)](https://styleci.io/repos/1192066315)[![codecov](https://camo.githubusercontent.com/f6a2fbdff259c467fe1dc25eace2fd22505f84f8c57fe0570580c4e7b56bd71b/68747470733a2f2f636f6465636f762e696f2f67682f746573746d6f6e69746f722f656c6f7175656e742d726576697361626c652f67726170682f62616467652e737667)](https://codecov.io/gh/testmonitor/eloquent-revisable)[![License](https://camo.githubusercontent.com/19601b94b95eb831478184540da352c0732c6b8f871c86c2bebf167a0e80d929/68747470733a2f2f706f7365722e707567782e6f72672f746573746d6f6e69746f722f656c6f7175656e742d726576697361626c652f6c6963656e7365)](https://packagist.org/packages/testmonitor/eloquent-revisable)

A Laravel package that provides revision tracking for Eloquent models. Add the `HasRevisions` trait to any model to automatically snapshot its state on every change, with support for field filtering, relation snapshots, revision limits, rollbacks, and event hooks.

Table of Contents
-----------------

[](#table-of-contents)

- [Installation](#installation)
- [Usage](#usage)
- [Examples](#examples)
    - [Configuration](#configuration)
    - [Reading revisions](#reading-revisions)
    - [Saving revisions](#saving-revisions)
    - [Rolling back](#rolling-back)
    - [Events &amp; control](#events--control)
- [Tests](#tests)
- [Changelog](#changelog)
- [Contributing](#contributing)
- [Credits](#credits)
- [License](#license)

Installation
------------

[](#installation)

Install the package via Composer:

```
$ composer require testmonitor/eloquent-revisable

```

Publish the config file and migration:

```
$ php artisan vendor:publish --provider="TestMonitor\Revisable\RevisableServiceProvider" --tag="config"
$ php artisan vendor:publish --provider="TestMonitor\Revisable\RevisableServiceProvider" --tag="migrations"

```

Once published, you can configure your user model, revision model, and name generator in `config/revisable.php`.

Run the migration to create the `revisions` table:

```
$ php artisan migrate

```

You're all set up now!

Usage
-----

[](#usage)

Add the `HasRevisions` trait to your Eloquent model and implement the `getRevisionOptions` method:

```
use TestMonitor\Revisable\Concerns\HasRevisions;
use TestMonitor\Revisable\RevisableOptions;

class Article extends Model
{
    use HasRevisions;

    public function getRevisionOptions(): RevisableOptions
    {
        return RevisableOptions::defaults();
    }
}
```

By default, a new revision is created every time the model is updated. The `RevisableOptions` fluent builder lets you control exactly what gets snapshotted and how.

Examples
--------

[](#examples)

### Configuration

[](#configuration)

Each model can be configured individually through `getRevisionOptions()`, independently of the global settings in `config/revisable.php`.

#### Creating a revision on model creation

[](#creating-a-revision-on-model-creation)

By default, revisions are only created on updates:

```
public function getRevisionOptions(): RevisableOptions
{
    return RevisableOptions::defaults()
        ->enableRevisionOnCreate();
}
```

#### Enabling and disabling revisioning

[](#enabling-and-disabling-revisioning)

Accepts a boolean or a callable, evaluated at revision time — suitable for feature flags or any other runtime condition:

```
public function getRevisionOptions(): RevisableOptions
{
    return RevisableOptions::defaults()
        ->enabledWhen(fn () => Feature::active('revision-tracking'));
}
```

`enabledWhen` controls whether revisions are created at all for a model. To suppress revisioning temporarily for a specific operation, use `withoutRevisioning()` instead — see [Suppressing revisioning](#suppressing-revisioning).

#### Tracking specific fields

[](#tracking-specific-fields)

By default all fields are tracked. Use `onlyFields` to include a specific set, or `exceptFields` to exclude certain fields and track everything else:

```
// Include only these fields
return RevisableOptions::defaults()
    ->onlyFields('title', 'body', 'status');

// Or exclude specific fields and track everything else
return RevisableOptions::defaults()
    ->exceptFields('views', 'cached_at');
```

#### Tracking relation snapshots

[](#tracking-relation-snapshots)

Capture the state of related models alongside field values:

```
public function getRevisionOptions(): RevisableOptions
{
    return RevisableOptions::defaults()
        ->withRelations('tags', 'categories');
}
```

> **Warning:** Rolling back a revision that includes relations will delete related records created after the snapshot was taken (or soft-delete them if the model uses `SoftDeletes`). Only opt in when you are prepared to handle this.

#### Excluding relations from restoration

[](#excluding-relations-from-restoration)

Relations are always tracked when listed in `withRelations()`, but you can prevent specific relations — or all of them — from being restored during a rollback. This is useful when a relation is managed by another system or process and should not be overwritten on rollback.

Exclude specific relations:

```
public function getRevisionOptions(): RevisableOptions
{
    return RevisableOptions::defaults()
        ->withRelations('author', 'tags')
        ->withoutRestoringRelations('tags'); // tags are tracked but never restored
}
```

Or exclude all relations from restoration:

```
public function getRevisionOptions(): RevisableOptions
{
    return RevisableOptions::defaults()
        ->withRelations('author', 'tags')
        ->withoutRestoringRelations(); // no relations are restored on rollback
}
```

Excluded relations are still snapshotted and visible in diffs — only the restoration step is skipped.

#### Tracking relation changes (optional)

[](#tracking-relation-changes-optional)

Laravel does not fire model events for certain relation operations, so the package provides two optional traits to fill those gaps. Both respect `withoutRevisioning()` and the `revisioning` event, and only trigger when the relation is listed in `withRelations()`.

> **Note:** Bulk query-builder operations such as `$article->attachments()->delete()` do not fire model events and will not trigger a revision regardless of which traits are used.

**Many-to-many (BelongsToMany / MorphToMany)**

`attach`, `detach`, `sync`, `toggle`, and `updateExistingPivot` bypass model events. Add `HasRevisionablePivots` to the parent to capture these changes:

```
use TestMonitor\Revisable\Concerns\HasRevisions;
use TestMonitor\Revisable\Concerns\HasRevisionablePivots;
use TestMonitor\Revisable\RevisableOptions;

class Article extends Model
{
    use HasRevisions, HasRevisionablePivots;

    public function getRevisionOptions(): RevisableOptions
    {
        return RevisableOptions::defaults()
            ->withRelations('tags');
    }
}
```

A revision is only triggered when the operation results in an actual change.

**HasOne, MorphOne, HasMany, MorphMany**

Child model saves and deletes do not bubble up to the parent as model events either. Add `HasRevisionableChildren` to the parent and `BelongsToRevisable` to each child model:

```
// Parent model
use TestMonitor\Revisable\Concerns\HasRevisions;
use TestMonitor\Revisable\Concerns\HasRevisionableChildren;
use TestMonitor\Revisable\RevisableOptions;

class Article extends Model
{
    use HasRevisions, HasRevisionableChildren;

    public function getRevisionOptions(): RevisableOptions
    {
        return RevisableOptions::defaults()
            ->withRelations('attachments');
    }

    public function attachments(): HasMany
    {
        return $this->hasMany(Attachment::class);
    }
}
```

```
// Child model
use TestMonitor\Revisable\Concerns\BelongsToRevisable;

class Attachment extends Model
{
    use BelongsToRevisable;

    public function article(): BelongsTo
    {
        return $this->belongsTo(Article::class);
    }
}
```

The child detects its revisable parent by scanning its `BelongsTo` / `MorphTo` methods and the parent's `HasOne` / `MorphOne` / `HasMany` / `MorphMany` methods. **Both sides must declare an explicit return type** — methods without one are skipped silently.

If you cannot add return types, override `revisableParent()` on the child to return the parent directly:

```
protected function revisableParent(): ?Model
{
    return $this->article;
}
```

#### Limiting the number of stored revisions

[](#limiting-the-number-of-stored-revisions)

Automatically prune the oldest revisions once the limit is reached:

```
public function getRevisionOptions(): RevisableOptions
{
    return RevisableOptions::defaults()
        ->limitRevisionsTo(10);
}
```

#### Living snapshots (replace instead of accumulate)

[](#living-snapshots-replace-instead-of-accumulate)

By default every save creates a new revision. When a model goes through many minor edits before reaching a stable state — such as a draft document — you may prefer to keep a single *living snapshot* that is overwritten on each save, rather than accumulating many interim revisions.

Use `replaceWhen` with a boolean or a callable that receives the model:

```
public function getRevisionOptions(): RevisableOptions
{
    return RevisableOptions::defaults()
        ->replaceWhen(fn ($model) => $model->status === 'draft');
}
```

When the condition is true the latest revision is updated in place; its identity (id, `created_at`) is preserved. When the condition is false a new revision is created as normal, so the transition out of draft becomes its own permanent entry in the history.

If no revision exists yet the first save always creates one, regardless of the condition.

Rollback revisions are never targeted for replacement — they are always preserved as permanent checkpoints regardless of the condition.

If a different user edits the model, a new revision is always created to preserve per-user attribution.

Use `replaceWithin` to limit replacement to a time window. Once the window has passed since the last save, the next edit produces a new revision instead:

```
public function getRevisionOptions(): RevisableOptions
{
    return RevisableOptions::defaults()
        ->replaceWhen(fn ($model) => $model->status === 'draft')
        ->replaceWithin(new \DateInterval('PT1H'));
}
```

The window is measured from the revision's last update, so it resets on every save within the window.

The living snapshot captures the pre-save state, consistent with normal revision behaviour. After two saves in draft, the snapshot holds the state before the most recent save, which serves as the rollback point.

#### Custom revision naming

[](#custom-revision-naming)

The default `VersionNameGenerator` names revisions sequentially (v1, v2, …). You can provide your own generator by implementing the `NameGenerator` contract and registering it in the options:

```
use TestMonitor\Revisable\Contracts\NameGenerator;

class TimestampNameGenerator implements NameGenerator
{
    public function generate(Model $model): string
    {
        return now()->toDateTimeString();
    }
}
```

```
public function getRevisionOptions(): RevisableOptions
{
    return RevisableOptions::defaults()
        ->nameRevisionUsing(new TimestampNameGenerator);
}
```

Pass `null` to disable automatic naming entirely:

```
return RevisableOptions::defaults()->nameRevisionUsing(null);
```

---

### Reading revisions

[](#reading-revisions)

Revisions are standard Eloquent models and can be queried directly on any revisionable model, or across all models using the built-in scopes.

#### Accessing revisions

[](#accessing-revisions)

All revisions are available via the `revisions` relationship:

```
$article = Article::find(1);

foreach ($article->revisions as $revision) {
    echo $revision->name . ' — ' . $revision->created_at . PHP_EOL;
}
```

Use `firstRevision` and `latestRevision` to jump directly to either end of the history:

```
$article->firstRevision;
$article->latestRevision;
```

#### Querying revisions

[](#querying-revisions)

Filter revisions using the built-in scopes:

```
// Revisions created by a specific user
$revisions = Revision::forUser($user)->get();

// All revisions for a specific model instance
$revisions = Revision::forModel($article)->get();

// Exclude rollback revisions
$revisions = $article->revisions()->notRollback()->get();

// Only rollback revisions
$revisions = $article->revisions()->onlyRollbacks()->get();
```

#### Reconstructing a model from a revision

[](#reconstructing-a-model-from-a-revision)

Any revision can be reconstructed as a model instance reflecting the state at the time it was captured:

```
$snapshot = $article->firstRevision->toModel(); // an Article instance, not a live record
echo $snapshot->title;
```

#### Comparing revisions

[](#comparing-revisions)

Use `diff()` to compare two states and inspect what changed. It returns a `Diff` object with `changes()` (only differing fields and relations) and `all()` (everything, including unchanged).

```
// What changed between two revisions
$diff = $revision->diff();              // vs its predecessor
$diff = $revision->diff($other);        // vs a specific revision

// What changed between the current model and a revision
$diff = $article->diff();               // vs the latest revision
$diff = $article->diff($revision);      // vs a specific revision
```

The output of `changes()` contains field entries and relation entries in one flat array:

```
$changes = $diff->changes();

// Field: ['old' => mixed, 'new' => mixed]
$changes['title'];    // ['old' => 'Draft', 'new' => 'Published']

// Relation: ['added' => [...ids], 'removed' => [...ids], 'changed' => [...]]
$changes['tags'];     // ['added' => [4], 'removed' => [1], 'changed' => []]
```

Use `all()` to include fields and relations that did not change:

```
$all = $diff->all();
```

---

### Saving revisions

[](#saving-revisions)

Revisions are created automatically on every save. Use `saveAsRevision()` when you need a named snapshot or want to attach additional context.

#### Manually saving a revision

[](#manually-saving-a-revision)

Save a named snapshot at any point without waiting for a model update, optionally with extra context:

```
$article->saveAsRevision('Before major refactor');

// Attach arbitrary key/value context via the properties argument
$article->saveAsRevision('Before major refactor', [
    'reason' => 'Restructuring content',
    'ticket' => 'PROJ-42',
]);
```

Properties are stored as JSON and available on the revision instance:

```
$revision->properties['ticket']; // 'PROJ-42'
```

---

### Rolling back

[](#rolling-back)

Any revision can be used to restore a model — and its tracked relations — to an earlier state.

#### Rolling back to the latest revision

[](#rolling-back-to-the-latest-revision)

To roll back a model to its most recent revision:

```
$article->rollback(); // returns false if no revisions exist
```

#### Rolling back to a specific revision

[](#rolling-back-to-a-specific-revision)

To restore a model to any earlier revision, pass the revision instance directly:

```
$article->rollbackToRevision($article->firstRevision);
```

#### Disabling revision creation on rollback

[](#disabling-revision-creation-on-rollback)

By default, every rollback creates a new revision capturing the restored state. Disable this per model:

```
public function getRevisionOptions(): RevisableOptions
{
    return RevisableOptions::defaults()
        ->disableRevisionOnRollback();
}
```

#### Rollback revisions

[](#rollback-revisions)

The revision created after a rollback is flagged with `rollback = true`. This has two effects:

- **It is never targeted by `replaceWhen`.** When a model uses living snapshots, subsequent edits replace the most recent regular revision — the rollback revision is always preserved as a permanent checkpoint.
- **It can be filtered using the built-in scopes** (`notRollback`, `onlyRollbacks`) — see [Querying revisions](#querying-revisions).

---

### Events &amp; control

[](#events--control)

The package fires events before and after revisioning and rollback. These can be used to add behaviour, abort operations, or integrate with other systems. Individual saves can also be excluded from revision tracking.

#### Listening to events

[](#listening-to-events)

The package fires four model events you can hook into directly or via an observer:

```
// Fires before a revision is created — return false to abort
Post::revisioning(function (Post $post): void {
    // ...
});

// Fires after a revision is created — access the revision via $post->latestRevision
Post::revisioned(function (Post $post): void {
    $post->notify(new PostRevisioned($post->latestRevision));
});

// Fires before a rollback — return false to abort
Post::rollingBack(function (Post $post): void {
    // ...
});

// Fires after a rollback
Post::rolledBack(function (Post $post): void {
    Cache::forget("post.{$post->id}");
});
```

An observer class is useful when handling multiple events on the same model:

```
class PostObserver
{
    public function revisioned(Post $post): void { ... }
    public function rolledBack(Post $post): void { ... }
}

// In a service provider:
Post::observe(PostObserver::class);
```

#### Suppressing revisioning

[](#suppressing-revisioning)

To run an operation without creating a revision:

```
$article->withoutRevisioning(function () use ($article) {
    $article->update(['views' => $article->views + 1]);
});
```

Tests
-----

[](#tests)

The package contains integration tests. You can run them using PHPUnit.

```
$ vendor/bin/phpunit

```

Changelog
---------

[](#changelog)

Refer to [CHANGELOG](CHANGELOG.md) for more information.

Contributing
------------

[](#contributing)

Refer to [CONTRIBUTING](CONTRIBUTING.md) for contributing details.

Credits
-------

[](#credits)

- **Thijs Kok** - *Lead developer* - [ThijsKok](https://github.com/thijskok)
- **Stephan Grootveld** - *Developer* - [Stefanius](https://github.com/stefanius)
- **Frank Keulen** - *Developer* - [FrankIsGek](https://github.com/frankisgek)

License
-------

[](#license)

The MIT License (MIT). Refer to the [License](LICENSE.md) for more information.

###  Health Score

25

—

LowBetter than 36% of packages

Maintenance63

Regular maintenance activity

Popularity10

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity17

Early-stage or recently created project

 Bus Factor1

Top contributor holds 90.7% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

### Community

Maintainers

![](https://www.gravatar.com/avatar/39f48c881813b7d3b044ca5660aa5ab9e60b5dd7c34ed4a47acbb11bd20b7593?d=identicon)[thijskok](/maintainers/thijskok)

---

Top Contributors

[![thijskok](https://avatars.githubusercontent.com/u/1344550?v=4)](https://github.com/thijskok "thijskok (88 commits)")[![stefanius](https://avatars.githubusercontent.com/u/2707905?v=4)](https://github.com/stefanius "stefanius (9 commits)")

### Embed Badge

![Health badge](/badges/testmonitor-eloquent-revisable/health.svg)

```
[![Health](https://phpackages.com/badges/testmonitor-eloquent-revisable/health.svg)](https://phpackages.com/packages/testmonitor-eloquent-revisable)
```

###  Alternatives

[jdorn/sql-formatter

a PHP SQL highlighting library

3.9k116.5M113](/packages/jdorn-sql-formatter)[propel/propel1

Propel is an open-source Object-Relational Mapping (ORM) for PHP5.

8351.6M87](/packages/propel-propel1)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
