PHPackages                             avocet-shores/laravel-rewind - 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. avocet-shores/laravel-rewind

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

avocet-shores/laravel-rewind
============================

Version control for Eloquent models with hybrid diff and snapshot storage.

v0.9.0(3mo ago)20814.8k—4.2%4[14 issues](https://github.com/avocet-shores/laravel-rewind/issues)[2 PRs](https://github.com/avocet-shores/laravel-rewind/pulls)MITPHPPHP ^8.3||^8.4CI passing

Since Jan 13Pushed 1w ago3 watchersCompare

[ Source](https://github.com/avocet-shores/laravel-rewind)[ Packagist](https://packagist.org/packages/avocet-shores/laravel-rewind)[ Docs](https://github.com/avocet-shores/laravel-rewind)[ GitHub Sponsors]()[ RSS](/packages/avocet-shores-laravel-rewind/feed)WikiDiscussions main Synced 3d ago

READMEChangelog (10)Dependencies (26)Versions (29)Used By (0)

Laravel Rewind
==============

[](#laravel-rewind)

[![Latest Version on Packagist](https://camo.githubusercontent.com/da2ae1b9a4681aea2785427967848522121d9df8f2efe038d59bfe495eaecdae/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f61766f6365742d73686f7265732f6c61726176656c2d726577696e642e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/avocet-shores/laravel-rewind)[![GitHub Tests Action Status](https://camo.githubusercontent.com/a931744e187370b4997324c7c5cb705d703beac587b22e2425c9b5aa2a119dfd/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f61766f6365742d73686f7265732f6c61726176656c2d726577696e642f72756e2d74657374732e796d6c3f6272616e63683d6d61696e266c6162656c3d7465737473267374796c653d666c61742d737175617265)](https://github.com/avocet-shores/laravel-rewind/actions?query=workflow%3Arun-tests+branch%3Amain)[![Coverage Status](https://camo.githubusercontent.com/c28f353c6a73e3774a8d8af4223ea867c989c2a9c1bca794ce3888e8217eb1c5/68747470733a2f2f696d672e736869656c64732e696f2f636f6465636f762f632f6769746875622f61766f6365742d73686f7265732f6c61726176656c2d726577696e643f7374796c653d666c61742d737175617265)](https://app.codecov.io/gh/avocet-shores/laravel-rewind/)[![GitHub Code Style Action Status](https://camo.githubusercontent.com/2a80b12ebfd846999a9cab7e892ff36e27f58a5af02ce7fb7ac1759c6715330a/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f61766f6365742d73686f7265732f6c61726176656c2d726577696e642f6669782d7068702d636f64652d7374796c652d6973737565732e796d6c3f6272616e63683d6d61696e266c6162656c3d636f64652532307374796c65267374796c653d666c61742d737175617265)](https://github.com/avocet-shores/laravel-rewind/actions?query=workflow%3A%22Fix+PHP+code+style+issues%22+branch%3Amain)[![Total Downloads](https://camo.githubusercontent.com/835bca2cb253f422bb11f81abacc8d59f909a93d6b6deb36e7052134b4f9c05b/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f61766f6365742d73686f7265732f6c61726176656c2d726577696e642e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/avocet-shores/laravel-rewind)

Full version control for your Eloquent models. Rewind, fast-forward, restore, diff, and query point-in-time state.

Under the hood, Rewind stores a mix of partial diffs and full snapshots. You get the storage efficiency of diffs with the reconstruction speed of snapshots, and the interval is configurable to suit your needs.

```
use AvocetShores\LaravelRewind\Facades\Rewind;

$post->update(['title' => 'Updated Title']);

Rewind::rewind($post);       // Back to 'Old Title'
Rewind::fastForward($post);  // Forward to 'Updated Title'
Rewind::goTo($post, 3);      // Jump to any version
Rewind::restore($post, 1);   // Create a new version from v1's state
```

Why Rewind?
-----------

[](#why-rewind)

- **Hybrid storage engine.** Diffs between snapshots keep storage small. Snapshots at configurable intervals keep reconstruction fast. You control the trade-off.
- **Thread-safe.** Cache-based locking prevents version sequence breaks during concurrent writes.
- **Non-destructive history.** Edits on older versions, restores, and pruning all preserve the full audit trail.
- **Batch versioning.** Group changes across multiple models into a single logical revision.
- **Built for real workloads.** Queued version creation, automatic pruning, and a cost-based approach engine that picks the fastest reconstruction path.

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

[](#installation)

```
composer require avocet-shores/laravel-rewind
```

Publish and run the migrations, and publish the config:

```
php artisan vendor:publish --provider="AvocetShores\LaravelRewind\LaravelRewindServiceProvider"
php artisan migrate
```

Getting Started
---------------

[](#getting-started)

### 1. Add the trait

[](#1-add-the-trait)

```
use AvocetShores\LaravelRewind\Traits\Rewindable;

class Post extends Model
{
   use Rewindable;
}
```

### 2. Add the `current_version` column

[](#2-add-the-current_version-column)

```
php artisan rewind:add-version
```

This generates a migration that adds `current_version` to your model's table. Run `php artisan migrate` to apply it.

That's it. Your model's changes are now tracked automatically.

Navigating History
------------------

[](#navigating-history)

```
use AvocetShores\LaravelRewind\Facades\Rewind;

// Step backward/forward
Rewind::rewind($post, 2);    // Go back 2 versions
Rewind::fastForward($post);  // Go forward 1 version

// Jump to a specific version
Rewind::goTo($post, 5);

// Get the model's state at a specific point in time
$attributes = Rewind::versionAt($post, Carbon::parse('2025-01-15 14:30:00'));
```

Restoring State
---------------

[](#restoring-state)

There are two ways to go back to a previous version, and the distinction matters:

`goTo()` moves the pointer. The model is updated to match the target version, but no new version record is created. Good for previewing or navigating.

`restore()` creates a new version with the target version's state. The history shows the restore happened. Good for audit trails and compliance.

```
// Move the pointer (no audit trail of the move itself)
Rewind::goTo($post, 3);

// Create a new version from v3's state (audit trail preserved)
Rewind::restore($post, 3);
// $post is now at v8 (or whatever the next version is), with v3's attributes
// The version record has event_type 'restored' and meta['restored_from_version'] = 3
```

Inspecting Changes
------------------

[](#inspecting-changes)

### Version history

[](#version-history)

```
$versions = $post->versions;
```

### Diff between two versions

[](#diff-between-two-versions)

```
$diff = Rewind::diff($post, 1, 5);

$diff->changed;   // ['title' => ['old' => 'Draft', 'new' => 'Published']]
$diff->added;     // Attributes only in v5
$diff->removed;   // Attributes only in v1
$diff->isEmpty(); // false
```

Works in either direction. `diff($post, 5, 1)` swaps old and new.

### Replay through history

[](#replay-through-history)

Walk through a range of versions with the fully reconstructed state at each step:

```
Rewind::replay($post, 1, 10, function (RewindVersion $version, array $attributes) {
    // $version is the RewindVersion record (with meta, event_type, etc.)
    // $attributes is the complete model state at that version
});
```

Callback return values are collected into a `Collection`, so you can use it as a map:

```
$titles = Rewind::replay($post, 1, 10, function (RewindVersion $version, array $attributes) {
    return $attributes['title'];
});

// Collection(['Draft', 'Review', 'Published', ...])
```

Works in reverse too. `replay($post, 10, 1)` walks backward from v10 to v1.

State is reconstructed incrementally, not independently per version, so this stays fast even over large ranges.

### Build a specific version's attributes

[](#build-a-specific-versions-attributes)

Diffs don't always contain all the data for a version. This method reconstructs the full attribute set:

```
$attributes = Rewind::getVersionAttributes($post, 7);
```

### Clone a model at a version

[](#clone-a-model-at-a-version)

```
$clone = Rewind::cloneModel($post, 5);
```

### Query scopes

[](#query-scopes)

```
use AvocetShores\LaravelRewind\Models\RewindVersion;
use AvocetShores\LaravelRewind\Enums\VersionEventType;

RewindVersion::forModel($post)->get();
RewindVersion::byUser($userId)->get();
RewindVersion::ofType(VersionEventType::Updated)->get();
RewindVersion::betweenDates($startDate, $endDate)->get();
RewindVersion::betweenVersions(1, 10)->get();

// Chain them together
RewindVersion::forModel($post)
    ->ofType(VersionEventType::Updated)
    ->byUser($userId)
    ->get();
```

Tracking State Transitions
--------------------------

[](#tracking-state-transitions)

If your model has fields that represent state (like an order's status or payment status), Rewind can track each transition structurally. You get a queryable history of when and how states changed, separate from general attribute versioning.

### Define state fields

[](#define-state-fields)

```
use AvocetShores\LaravelRewind\Traits\Rewindable;

class Order extends Model
{
   use Rewindable;

   protected array $rewindStateFields = ['status', 'payment_status'];
}
```

Only fields listed in `$rewindStateFields` are tracked as transitions. All other attributes continue to be versioned normally.

### Querying transitions

[](#querying-transitions)

```
// Find versions where status became 'shipped'
$order->versions()->whereStateBecame('status', 'shipped')->get();

// Find versions where status transitioned away from 'pending'
$order->versions()->whereStateWas('status', 'pending')->get();

// Find every version where status changed at all
$order->versions()->whereStateChanged('status')->get();

// Match an exact from/to transition
$order->versions()->whereStateTransition('status', 'pending', 'shipped')->get();
```

`whereStateTransition` supports wildcards. Pass `null` for either direction to match any value:

```
// Any transition that ended at 'shipped', regardless of where it came from
$order->versions()->whereStateTransition('status', null, 'shipped')->get();
```

These compose with existing scopes:

```
$order->versions()
    ->whereStateBecame('status', 'shipped')
    ->byUser($userId)
    ->get();
```

### State history

[](#state-history)

Get a clean timeline of transitions for a specific field:

```
$history = $order->stateHistory('status');

// [
//     ['version' => 1, 'from' => null,       'to' => 'pending',    'created_at' => ...],
//     ['version' => 2, 'from' => 'pending',   'to' => 'processing', 'created_at' => ...],
//     ['version' => 3, 'from' => 'processing', 'to' => 'shipped',   'created_at' => ...],
// ]
```

> State transitions work with amend mode. If multiple changes to a state field happen inside `amendCurrentVersion`, the transition collapses to the original `from` and the final `to`.

Controlling What's Tracked
--------------------------

[](#controlling-whats-tracked)

### Exclude attributes

[](#exclude-attributes)

```
public static function excludedFromVersioning(): array
{
    return ['password', 'api_token'];
}
```

### Amend the current version

[](#amend-the-current-version)

Sometimes you want to save a change without creating a new version. Maybe you're bumping a counter or syncing a denormalized field.

```
Rewind::amendCurrentVersion(function () {
    $post->update(['view_count' => $post->view_count + 1]);
});
```

The changed attributes are folded into the current version's `old_values` and `new_values`. No new version row is created, but `goTo()`, `rewind()`, and `diff()` still work as expected.

> If an attribute should *never* appear in version history, use `excludedFromVersioning()` instead. `amendCurrentVersion` is for attributes you still want tracked, just not as a separate version.

### Attach metadata

[](#attach-metadata)

Record why a change was made:

```
Rewind::withMeta(['reason' => 'Bulk price update', 'ticket' => 'JIRA-123']);
$product->update(['price' => 29.99]);
```

Metadata is stored in the version's `meta` field and automatically cleared after version creation.

### Event type tracking

[](#event-type-tracking)

Each version records the event that created it: `created`, `updated`, `deleted`, or `restored`.

```
$creates = $post->versions()->where('event_type', VersionEventType::Created->value)->get();
```

### Initialize a v1 without changes

[](#initialize-a-v1-without-changes)

If you have an existing model and want to create a baseline version record:

```
$post->initVersion();
```

Working With Multiple Models
----------------------------

[](#working-with-multiple-models)

Batch versioning groups changes across models under a shared identifier:

```
$batchUuid = Rewind::batch(function () {
    $order->update(['status' => 'shipped']);
    $item->update(['shipped_at' => now()]);
});

// Query all versions in the batch
$versions = RewindVersion::inBatch($batchUuid)->get();
```

Managing Storage
----------------

[](#managing-storage)

### Pruning old versions

[](#pruning-old-versions)

```
# Keep the last 50 versions per model
php artisan rewind:prune --keep=50

# Delete versions older than a year
php artisan rewind:prune --days=365

# Combine both (--keep protects recent versions regardless of age)
php artisan rewind:prune --keep=50 --days=365

# Prune a specific model type
php artisan rewind:prune --keep=50 --model=App\\Models\\Post

# Dry run
php artisan rewind:prune --keep=50 --pretend
```

When versions are pruned, Rewind automatically converts the new oldest remaining version into a full snapshot so navigation continues to work.

Schedule it:

```
Schedule::command('rewind:prune --keep=50 --force')->daily();
```

You can set defaults for `--keep` and `--days` in `config/rewind.php` via `prune_keep_versions` and `prune_older_than_days`.

### Automatic version limits

[](#automatic-version-limits)

Cap versions per model:

```
class Post extends Model
{
    use Rewindable;

    protected static int $maxRewindVersions = 30;
}
```

Or set a global default via the `max_versions` config key. The per-model property takes precedence.

Configuration
-------------

[](#configuration)

### Custom version model

[](#custom-version-model)

Extend `RewindVersion` with your own model:

```
// config/rewind.php
'version_model' => App\Models\CustomRewindVersion::class,
```

Your model must extend `AvocetShores\LaravelRewind\Models\RewindVersion`.

### Queued version creation

[](#queued-version-creation)

For high-write models, dispatch version creation to a queue:

```
// config/rewind.php
'listener_should_queue' => true,
```

Queue retry behavior is configurable via the `queue` config key.

### Lock timeout handling

[](#lock-timeout-handling)

When a cache lock can't be acquired, behavior is configurable via `on_lock_timeout`:

- `log` (default): Logs an error silently.
- `event`: Dispatches a `RewindVersionLockTimeout` event for custom handling.
- `throw`: Throws a `LockTimeoutRewindException`. Useful with queued listeners since it triggers Laravel's retry mechanism.

### Snapshot interval

[](#snapshot-interval)

Controls how often full snapshots are stored vs. partial diffs. Default is every 10 versions. Higher values save storage at the cost of longer reconstruction times.

```
// config/rewind.php
'snapshot_interval' => 10,
```

How It Works
------------

[](#how-it-works)

Rewind maintains a linear, non-destructive history. Here's what happens when you edit a model while on an older version:

1. Create a post, then update it. You're at v2.
2. Rewind to v1.
3. Update the post again.

Rewind uses the previous head version (v2) as the `old_values` for the new version (v3), creates a full snapshot, and marks v3 as the new head:

```
[
    'version' => 3,
    'old_values' => [
        'title' => 'New Title', // From v2, not v1
    ],
    'new_values' => [
        'title' => 'Rewind is Awesome!',
    ],
]
```

The history always reads as if you updated from the previous head. You can jump around freely without losing data.

Testing
-------

[](#testing)

```
composer test
```

Changelog
---------

[](#changelog)

Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.

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

[](#contributing)

Please see [CONTRIBUTING](CONTRIBUTING.md) for details.

Security Vulnerabilities
------------------------

[](#security-vulnerabilities)

Please review [our security policy](../../security/policy) on how to report security vulnerabilities.

Credits
-------

[](#credits)

- [Jared Cannon](https://github.com/jared-cannon)
- [All Contributors](../../contributors)

License
-------

[](#license)

The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

###  Health Score

55

—

FairBetter than 97% of packages

Maintenance89

Actively maintained with recent releases

Popularity43

Moderate usage in the ecosystem

Community17

Small or concentrated contributor base

Maturity56

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 88.8% 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.

###  Release Activity

Cadence

Every ~26 days

Recently: every ~99 days

Total

18

Last Release

95d ago

PHP version history (2 changes)0.1.0PHP ^8.3

0.7.3PHP ^8.3||^8.4

### Community

Maintainers

![](https://www.gravatar.com/avatar/8e39a4652a6267d9308f777fa82762f72a73d908a2525880e10fa67a1f0466e4?d=identicon)[jared-cannon](/maintainers/jared-cannon)

---

Top Contributors

[![jared-cannon](https://avatars.githubusercontent.com/u/60587282?v=4)](https://github.com/jared-cannon "jared-cannon (150 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (9 commits)")[![github-actions[bot]](https://avatars.githubusercontent.com/in/15368?v=4)](https://github.com/github-actions[bot] "github-actions[bot] (6 commits)")[![danielrona](https://avatars.githubusercontent.com/u/1699775?v=4)](https://github.com/danielrona "danielrona (1 commits)")[![fdjkgh580](https://avatars.githubusercontent.com/u/4414580?v=4)](https://github.com/fdjkgh580 "fdjkgh580 (1 commits)")[![nilshee](https://avatars.githubusercontent.com/u/13719337?v=4)](https://github.com/nilshee "nilshee (1 commits)")[![Suraj80](https://avatars.githubusercontent.com/u/66175409?v=4)](https://github.com/Suraj80 "Suraj80 (1 commits)")

---

Tags

eloquenteloquent-modelseloquent-ormlaravellaravel-frameworklaravel-packageversioninglaravelversioningrewindAvocet Shoreslaravel-rewind

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/avocet-shores-laravel-rewind/health.svg)

```
[![Health](https://phpackages.com/badges/avocet-shores-laravel-rewind/health.svg)](https://phpackages.com/packages/avocet-shores-laravel-rewind)
```

###  Alternatives

[spatie/laravel-pdf

Create PDFs in Laravel apps

1.0k4.8M47](/packages/spatie-laravel-pdf)[dedoc/scramble

Automatic generation of API documentation for Laravel applications.

2.1k11.2M100](/packages/dedoc-scramble)[wnx/laravel-backup-restore

A package to restore database backups made with spatie/laravel-backup.

213421.0k2](/packages/wnx-laravel-backup-restore)[spatie/laravel-passkeys

Use passkeys in your Laravel app

471890.7k39](/packages/spatie-laravel-passkeys)[rawilk/profile-filament-plugin

Profile &amp; MFA starter kit for filament.

3914.6k](/packages/rawilk-profile-filament-plugin)[lacodix/laravel-model-filter

A Laravel package to filter, search and sort models with ease while fetching from database.

17558.6k](/packages/lacodix-laravel-model-filter)

PHPackages © 2026

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