avocet-shores / laravel-rewind
Version control for Eloquent models with hybrid diff and snapshot storage.
Fund package maintenance!
Requires
- php: ^8.3||^8.4
- illuminate/contracts: ^10.0||^11.0||^12.0
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- larastan/larastan: ^2.9
- laravel/pint: ^1.14
- nunomaduro/collision: ^8.1.1||^7.10.0
- orchestra/testbench: ^9.0.0||^8.22.0
- pestphp/pest: ^3.0
- pestphp/pest-plugin-arch: ^3.0
- pestphp/pest-plugin-laravel: ^3.0
- phpstan/extension-installer: ^1.3
- phpstan/phpstan-deprecation-rules: ^1.1
- phpstan/phpstan-phpunit: ^1.3
- spatie/laravel-ray: ^1.35
- dev-main
- v0.9.0
- v0.8.0
- 0.7.4
- 0.7.3
- 0.7.2
- 0.7.1
- 0.7.0
- 0.6.0
- 0.5.0
- 0.4.0
- 0.3.0
- 0.2.2
- 0.2.1
- 0.2.0
- 0.1.6
- 0.1.4
- 0.1.2
- 0.1.0
- dev-claude/state-machine-integration-qjE4P
- dev-claude/point-in-time-query-scope-asof-3sWzh
- dev-docs/readme-rewrite
- dev-feature/v1.0.0-rc.1
- dev-feature/v0.8.0
- dev-claude/fix-soft-delete-test-c7gwR
- dev-claude/optimize-version-pruning-a95KD
- dev-claude/fix-config-and-uuid-pks-OvS3d
- dev-claude/audit-v1-readiness-zKyf9
- dev-claude/add-prune-command-E7lXd
- dev-claude/fix-lock-timeout-handling-wddWo
- dev-claude/add-composite-index-75R6e
- dev-dependabot/github_actions/dependabot/fetch-metadata-3.0.0
- dev-dependabot/github_actions/codecov/codecov-action-6
- dev-dependabot/github_actions/ramsey/composer-install-4
- dev-dependabot/github_actions/actions/checkout-6
- dev-claude/review-repo-priorities-011CUM4FfVHVnCk5B2Wq96it
- dev-claude/review-idea-submission-011CUM2hsfksX7q7a7Zs54zV
- dev-dependabot/github_actions/stefanzweifel/git-auto-commit-action-7
This package is auto-updated.
Last update: 2026-04-01 23:11:38 UTC
README
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?
- 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
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
1. Add the trait
use AvocetShores\LaravelRewind\Traits\Rewindable; class Post extends Model { use Rewindable; }
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
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
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
Version history
$versions = $post->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
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 = Rewind::cloneModel($post, 5);
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();
Controlling What's Tracked
Exclude attributes
public static function excludedFromVersioning(): array { return ['password', 'api_token']; }
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.amendCurrentVersionis for attributes you still want tracked, just not as a separate version.
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
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
If you have an existing model and want to create a baseline version record:
$post->initVersion();
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
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
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
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
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
When a cache lock can't be acquired, behavior is configurable via on_lock_timeout:
log(default): Logs an error silently.event: Dispatches aRewindVersionLockTimeoutevent for custom handling.throw: Throws aLockTimeoutRewindException. Useful with queued listeners since it triggers Laravel's retry mechanism.
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
Rewind maintains a linear, non-destructive history. Here's what happens when you edit a model while on an older version:
- Create a post, then update it. You're at v2.
- Rewind to v1.
- 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
composer test
Changelog
Please see CHANGELOG for more information on what has changed recently.
Contributing
Please see CONTRIBUTING for details.
Security Vulnerabilities
Please review our security policy on how to report security vulnerabilities.
Credits
License
The MIT License (MIT). Please see License File for more information.