avocet-shores/laravel-rewind
Full version control for Eloquent models: rewind, fast-forward, restore, diff, and query point-in-time state. Uses a hybrid engine (diffs + snapshots) with configurable intervals, thread-safe locking, batch revisions, queued writes, and pruning.
Installation:
composer require avocet-shores/laravel-rewind
php artisan vendor:publish --provider="AvocetShores\LaravelRewind\LaravelRewindServiceProvider"
php artisan migrate
Enable Versioning:
Add the Rewindable trait to your Eloquent model:
use AvocetShores\LaravelRewind\Traits\Rewindable;
class Post extends Model
{
use Rewindable;
}
Add Version Column:
Run the migration to add the current_version column:
php artisan rewind:add-version
php artisan migrate
Track Changes and Rewind:
$post = Post::find(1);
$post->update(['title' => 'Updated Title']);
// Rewind to previous state
Rewind::rewind($post); // Back to 'Old Title'
// Fast-forward to latest
Rewind::fastForward($post); // Back to 'Updated Title'
Version Navigation:
// Jump to a specific version
Rewind::goTo($post, 3);
// Get attributes at a specific version
$attributes = Rewind::versionAt($post, Carbon::parse('2025-01-15'));
State Restoration:
// Preview without audit trail
Rewind::goTo($post, 2);
// Create a new version from an old state (audit trail preserved)
Rewind::restore($post, 2);
Change Inspection:
// Diff between versions
$diff = Rewind::diff($post, 1, 3);
// Replay history
Rewind::replay($post, 1, 5, function ($version, $attributes) {
// Process each version
});
State Transitions:
Track critical fields (e.g., status) separately:
class Order extends Model
{
use Rewindable;
protected array $rewindStateFields = ['status', 'payment_status'];
}
Query transitions:
$order->versions()->whereStateBecame('status', 'shipped')->get();
Batch Versioning: Group related changes (e.g., order + items):
$batchUuid = Rewind::batch(function () {
$order->update(['status' => 'shipped']);
$item->update(['shipped_at' => now()]);
});
Exclude Sensitive Data:
public static function excludedFromVersioning(): array
{
return ['password', 'api_token'];
}
Metadata Attachment:
Rewind::withMeta(['reason' => 'Bulk update', 'ticket' => 'JIRA-123']);
$post->update(['title' => 'New Title']);
For high-traffic models, enable queued version creation:
// config/rewind.php
'listener_should_queue' => true,
Lock Timeouts:
Concurrent writes may fail if cache locks time out. Configure handling in config/rewind.php:
'on_lock_timeout' => 'event', // Options: 'log', 'event', 'throw'
Snapshot Interval:
Higher values (e.g., snapshot_interval: 20) save storage but slow down reconstruction. Default is 10.
Amend vs. Exclude:
Use amendCurrentVersion() for non-versioned changes (e.g., counters). Use excludedFromVersioning() for permanently hidden fields.
Pruning Side Effects:
Pruning converts the new oldest version into a snapshot. Test with --pretend first:
php artisan rewind:prune --keep=50 --pretend
Version Reconstruction:
If Rewind::getVersionAttributes() returns incomplete data, check the snapshot_interval or manually trigger a snapshot:
Rewind::forceSnapshot($post);
Batch Queries:
Ensure batch_uuid is included in queries for grouped versions:
RewindVersion::inBatch($batchUuid)->get();
State Transitions:
If transitions are missing, verify $rewindStateFields is correctly defined.
Custom Version Model:
Extend RewindVersion for additional fields:
// config/rewind.php
'version_model' => App\Models\CustomRewindVersion::class,
Event Listeners:
Listen for version events (e.g., RewindVersionCreated):
event(new RewindVersionCreated($version));
Pruning Logic:
Override default pruning behavior by extending the PruneVersions command.
snapshot_interval for read-heavy workloads (e.g., 20).'listener_should_queue' => true for write-heavy models.rewind_versions for large datasets:
Schema::table('rewind_versions', function (Blueprint $table) {
$table->index('model_type');
$table->index('model_id');
$table->index(['model_type', 'model_id', 'version']);
});
amendCurrentVersion:
Avoid for critical changes; use restore() instead to preserve audit trails.rewind:prune to avoid storage bloat.status) with non-state fields in $rewindStateFields.How can I help you explore Laravel packages today?