Skip to content

Basic Usage

This guide covers the revision tracking system in detail, including configuration options, querying revisions, and reverting changes.

Add the HasRevisions trait and implement the Traceable interface:

use Cline\Tracer\Concerns\HasRevisions;
use Cline\Tracer\Contracts\Traceable;
use Illuminate\Database\Eloquent\Model;
class Article extends Model implements Traceable
{
use HasRevisions;
protected $fillable = ['title', 'content', 'status', 'author_id'];
}

Once configured, Tracer automatically tracks:

EventAction RecordedOld ValuesNew Values
createdcreatedEmptyAll tracked attributes
updatedupdatedChanged attributes (old)Changed attributes (new)
deleteddeletedAll tracked attributesEmpty
forceDeletedforce_deletedAll tracked attributesEmpty
restoredrestoredEmptyAll tracked attributes
RevertrevertedCurrent stateReverted state
// All these are tracked automatically
$article = Article::create(['title' => 'Hello', 'content' => 'World']);
// Revision 1: action=created
$article->update(['title' => 'Hello World']);
// Revision 2: action=updated, old={title: 'Hello'}, new={title: 'Hello World'}
$article->delete();
// Revision 3: action=deleted

Configuration is managed via config/tracer.php or runtime registration, not on the model itself.

config/tracer.php
'models' => [
App\Models\Article::class => [
'tracked_attributes' => ['title', 'content', 'status'],
'untracked_attributes' => ['internal_notes'],
],
App\Models\User::class => [
'untracked_attributes' => ['password', 'api_token'],
],
],
use Cline\Tracer\Tracer;
// Track specific attributes only
Tracer::configure(Article::class)
->trackAttributes(['title', 'content', 'status']);
// Exclude specific attributes
Tracer::configure(User::class)
->untrackAttributes(['password', 'api_token']);

Configure in config/tracer.php:

'untracked_attributes' => [
'id',
'created_at',
'updated_at',
'deleted_at',
'remember_token',
'password', // Add custom global exclusions
'api_token',
],
// Via relationship (ordered by version descending)
$revisions = $article->revisions;
// Via facade
$revisions = Tracer::revisions($article)->all();
$latest = $article->latestRevision();
// or
$latest = Tracer::revisions($article)->latest();
$revision = $article->getRevision(3); // Get version 3
use Cline\Tracer\Enums\RevisionAction;
$updates = $article->revisions()
->where('action', RevisionAction::Updated)
->get();
// Revisions made by a specific user
$userRevisions = $article->revisions()
->where('causer_type', User::class)
->where('causer_id', $userId)
->get();
$recentRevisions = $article->revisions()
->where('created_at', '>=', now()->subDays(7))
->get();
$revision = $article->latestRevision();
// Get all old values
$oldValues = $revision->old_values;
// ['title' => 'Old Title']
// Get all new values
$newValues = $revision->new_values;
// ['title' => 'New Title']
// Check if specific attribute changed
if ($revision->hasChangedAttribute('title')) {
$oldTitle = $revision->getOldValue('title');
$newTitle = $revision->getNewValue('title');
}
$descriptions = $revision->describe();
// [
// 'title' => 'Changed from "Old Title" to "New Title"',
// 'status' => 'Set to "published"',
// ]
$revision = $article->latestRevision();
// Who made the change
$causer = $revision->causer; // User model (polymorphic)
// When
$when = $revision->created_at;
// Action type
$action = $revision->action; // RevisionAction enum
// Version number
$version = $revision->version; // int
use Cline\Tracer\Tracer;
// Revert to version 3
Tracer::revisions($article)->revertTo(3);
// Or via the TracerManager directly
Tracer::revertTo($article, 3);
$targetRevision = $article->revisions()->where('version', 3)->first();
Tracer::revisions($article)->revertTo($targetRevision);
Tracer::revisions($article)->revertTo('01HQ4XYZABC...'); // ULID/UUID
  1. Tracer reconstructs the model state at the target revision
  2. Applies those values to the current model
  3. Saves the model (without tracking this save)
  4. Creates a new “reverted” revision recording the change
$article->revertToRevision(2);
$latest = $article->latestRevision();
$latest->action; // RevisionAction::Reverted
$latest->metadata; // ['reverted_to_version' => 2]
use Cline\Tracer\Tracer;
Tracer::revisions($article)->withoutTracking(function () use ($article) {
$article->update(['view_count' => $article->view_count + 1]);
});
use Cline\Tracer\Tracer;
Tracer::revisions($article)->disableTracking();
$article->update(['internal_notes' => 'Not tracked']);
Tracer::revisions($article)->enableTracking();
use Cline\Tracer\Tracer;
$article = Article::find(1);
Tracer::revisions($article)->disableTracking();
// All changes to this instance are untracked
$article->update(['status' => 'archived']);
$article->update(['title' => 'New Title']);

By default, Tracer uses the authenticated user as the causer via the AuthCauserResolver. You can create a custom resolver:

use Cline\Tracer\Contracts\CauserResolver;
use Illuminate\Database\Eloquent\Model;
class CustomCauserResolver implements CauserResolver
{
public function resolve(): ?Model
{
// Use system user for automated changes
if (app()->runningInConsole()) {
return User::where('email', 'system@example.com')->first();
}
// Use API token owner for API requests
if (request()->bearerToken()) {
return PersonalAccessToken::findToken(request()->bearerToken())?->tokenable;
}
return auth()->user();
}
}

Register in config/tracer.php:

'causer_resolver' => CustomCauserResolver::class,

Configure via config/tracer.php:

'models' => [
App\Models\Article::class => [
'revision_diff_strategy' => AttributeDiffStrategy::class,
],
],

Or at runtime:

use Cline\Tracer\Tracer;
use Cline\Tracer\Strategies\Diff\AttributeDiffStrategy;
Tracer::configure(Article::class)
->revisionDiffStrategy(AttributeDiffStrategy::class);

Tracer automatically detects soft deletes:

use Illuminate\Database\Eloquent\SoftDeletes;
class Article extends Model implements Traceable
{
use HasRevisions;
use SoftDeletes;
}
OperationRecorded Action
$article->delete()deleted
$article->forceDelete()force_deleted
$article->restore()restored
// Load revisions with articles
$articles = Article::with('revisions')->get();
// Load only recent revisions
$articles = Article::with(['revisions' => function ($query) {
$query->where('created_at', '>=', now()->subMonth())->limit(10);
}])->get();

The migrations include indexes on:

  • traceable_type + traceable_id (composite)
  • version
  • action
  • causer_type + causer_id (composite)
  • created_at
// Delete revisions older than 1 year
Revision::where('created_at', '<', now()->subYear())->delete();
// Keep only last 100 revisions per model
$article->revisions()
->orderByDesc('version')
->skip(100)
->take(PHP_INT_MAX)
->delete();

Tracer dispatches events for each revision:

use Cline\Tracer\Events\RevisionCreated;
class RevisionListener
{
public function handle(RevisionCreated $event): void
{
$revision = $event->revision;
$model = $revision->traceable;
// Send notification, log to external system, etc.
Log::info("Revision {$revision->version} created for {$model->getKey()}");
}
}

Register in EventServiceProvider:

protected $listen = [
RevisionCreated::class => [
RevisionListener::class,
],
];