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
============================

Laravel Rewind is a powerful, easy-to-use versioning package for your Eloquent models.

0.7.4(6mo ago)19810.5k↓11.7%4[15 issues](https://github.com/avocet-shores/laravel-rewind/issues)[3 PRs](https://github.com/avocet-shores/laravel-rewind/pulls)MITPHPPHP ^8.3||^8.4CI passing

Since Jan 13Pushed 1mo 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 1mo ago

READMEChangelog (10)Dependencies (13)Versions (22)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.

### 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

51

—

FairBetter than 96% of packages

Maintenance79

Regular maintenance activity

Popularity42

Moderate usage in the ecosystem

Community17

Small or concentrated contributor base

Maturity55

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 90.2% 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 ~20 days

Recently: every ~71 days

Total

16

Last Release

193d 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 (148 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (8 commits)")[![github-actions[bot]](https://avatars.githubusercontent.com/in/15368?v=4)](https://github.com/github-actions[bot] "github-actions[bot] (5 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)")

---

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

[dyrynda/laravel-model-uuid

This package allows you to easily work with UUIDs in your Laravel models.

4802.8M8](/packages/dyrynda-laravel-model-uuid)[spatie/laravel-model-flags

Add flags to Eloquent models

4301.1M1](/packages/spatie-laravel-model-flags)[clickbar/laravel-magellan

This package provides functionality for working with the postgis extension in Laravel.

423715.4k1](/packages/clickbar-laravel-magellan)[spatie/laravel-sql-commenter

Add comments to SQL queries made by Laravel

1931.4M1](/packages/spatie-laravel-sql-commenter)[spatie/laravel-deleted-models

Automatically copy deleted records to a separate table

409109.8k4](/packages/spatie-laravel-deleted-models)[wnx/laravel-backup-restore

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

203330.1k2](/packages/wnx-laravel-backup-restore)

PHPackages © 2026

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