Advanced Usage
This guide covers advanced features including events, custom configurations, performance optimization, and integration patterns.
Events
Section titled “Events”Tracer dispatches events throughout the lifecycle of revisions and staged changes.
Available Events
Section titled “Available Events”| Event | When Dispatched |
|---|---|
RevisionCreated | After a revision is created |
StagedChangeCreated | After a staged change is created |
StagedChangeApproved | After a staged change is approved |
StagedChangeRejected | After a staged change is rejected |
StagedChangeApplied | After a staged change is applied |
Listening to Events
Section titled “Listening to Events”use Cline\Tracer\Events\RevisionCreated;use Cline\Tracer\Events\StagedChangeApproved;use Cline\Tracer\Events\StagedChangeRejected;
protected $listen = [ RevisionCreated::class => [ SendAuditNotification::class, ], StagedChangeApproved::class => [ NotifyAuthorOfApproval::class, ], StagedChangeRejected::class => [ NotifyAuthorOfRejection::class, ],];Event Payloads
Section titled “Event Payloads”use Cline\Tracer\Events\RevisionCreated;
class SendAuditNotification{ public function handle(RevisionCreated $event): void { $revision = $event->revision; $model = $revision->traceable; $causer = $revision->causer;
AuditLog::create([ 'model_type' => $model->getMorphClass(), 'model_id' => $model->getKey(), 'action' => $revision->action->value, 'user_id' => $causer?->id, 'changes' => $revision->new_values, ]); }}Disabling Events
Section titled “Disabling Events”'events' => [ 'enabled' => false,],Or temporarily:
Config::set('tracer.events.enabled', false);$model->update(['title' => 'Silent update']);Config::set('tracer.events.enabled', true);Morph Key Maps
Section titled “Morph Key Maps”Configure polymorphic type mappings for cleaner database values:
'morphKeyMap' => [ App\Models\User::class => 'user', App\Models\Article::class => 'article', App\Models\Document::class => 'document',],This stores user instead of App\Models\User in traceable_type columns.
Enforce Morph Map
Section titled “Enforce Morph Map”For strict control, use enforceMorphKeyMap:
'enforceMorphKeyMap' => [ App\Models\User::class => 'user', App\Models\Article::class => 'article',],This replaces Laravel’s morph map entirely for Tracer operations.
Conductors API
Section titled “Conductors API”Conductors provide a fluent interface for complex operations.
Revision Conductor
Section titled “Revision Conductor”use Cline\Tracer\Tracer;
$conductor = Tracer::revisions($article);
// Query revisions$all = $conductor->all();$latest = $conductor->latest();$version3 = $conductor->version(3);
// Filter by action$updates = $conductor->query() ->where('action', RevisionAction::Updated) ->get();
// Revert$conductor->revertTo(3);Staging Conductor
Section titled “Staging Conductor”$conductor = Tracer::staging($article);
// Query staged changes$pending = $conductor->pending();$approved = $conductor->approved();$all = $conductor->all();
// Actions$conductor->approve($stagedChange, $approver, 'Comment');$conductor->reject($stagedChange, $rejector, 'Reason');$conductor->apply($stagedChange, $applier);Batch Operations
Section titled “Batch Operations”Batch Revision Queries
Section titled “Batch Revision Queries”// Get revisions for multiple models$articles = Article::whereIn('id', [1, 2, 3])->get();
$allRevisions = Revision::query() ->whereIn('traceable_id', $articles->pluck('id')) ->where('traceable_type', Article::class) ->orderByDesc('created_at') ->get();Batch Apply Staged Changes
Section titled “Batch Apply Staged Changes”// Apply all approved changes across all models$approved = StagedChange::query() ->where('status', StagedChangeStatus::Approved) ->get();
foreach ($approved as $staged) { try { $staged->apply(auth()->user()); } catch (CannotApplyStagedChangeException $e) { Log::warning("Failed to apply staged change {$staged->id}: {$e->getMessage()}"); }}Performance Optimization
Section titled “Performance Optimization”Eager Loading
Section titled “Eager Loading”// Load revisions with models$articles = Article::with('revisions')->get();
// Load with limits$articles = Article::with(['revisions' => function ($query) { $query->orderByDesc('version')->limit(5);}])->get();Chunked Processing
Section titled “Chunked Processing”// Process revisions in chunksRevision::where('created_at', '<', now()->subYear()) ->chunkById(1000, function ($revisions) { foreach ($revisions as $revision) { // Archive to cold storage } $revisions->each->delete(); });Indexing
Section titled “Indexing”Ensure proper indexes exist (included in migrations):
-- RevisionsCREATE INDEX idx_revisions_traceable ON revisions(traceable_type, traceable_id);CREATE INDEX idx_revisions_causer ON revisions(causer_type, causer_id);CREATE INDEX idx_revisions_created ON revisions(created_at);
-- Staged ChangesCREATE INDEX idx_staged_stageable ON staged_changes(stageable_type, stageable_id);CREATE INDEX idx_staged_status ON staged_changes(status);CREATE INDEX idx_staged_author ON staged_changes(author_type, author_id);Pruning and Maintenance
Section titled “Pruning and Maintenance”Prune Old Revisions
Section titled “Prune Old Revisions”// Delete revisions older than 1 year$deleted = Revision::where('created_at', '<', now()->subYear())->delete();
// Keep only last N revisions per model$models = Revision::select('traceable_type', 'traceable_id') ->groupBy('traceable_type', 'traceable_id') ->get();
foreach ($models as $model) { Revision::where('traceable_type', $model->traceable_type) ->where('traceable_id', $model->traceable_id) ->orderByDesc('version') ->skip(100) ->take(PHP_INT_MAX) ->delete();}Scheduled Cleanup
Section titled “Scheduled Cleanup”protected function schedule(Schedule $schedule): void{ $schedule->call(function () { // Prune revisions older than config value $days = config('tracer.retention_days', 365); Revision::where('created_at', '<', now()->subDays($days))->delete();
// Clean up orphaned staged changes StagedChange::whereIn('status', [ StagedChangeStatus::Cancelled, StagedChangeStatus::Rejected, ])->where('updated_at', '<', now()->subDays(30))->delete(); })->daily();}Testing
Section titled “Testing”Using Array Driver (In-Memory)
Section titled “Using Array Driver (In-Memory)”For tests, disable database storage:
protected function setUp(): void{ parent::setUp();
// Use in-memory storage for tests Config::set('tracer.events.enabled', false);}Testing Revisions
Section titled “Testing Revisions”use Cline\Tracer\Database\Models\Revision;
test('creates revision on update', function () { $article = Article::create(['title' => 'Original']);
$article->update(['title' => 'Updated']);
expect($article->revisions)->toHaveCount(2);
$latest = $article->latestRevision(); expect($latest->action)->toBe(RevisionAction::Updated); expect($latest->old_values)->toHaveKey('title', 'Original'); expect($latest->new_values)->toHaveKey('title', 'Updated');});Testing Staged Changes
Section titled “Testing Staged Changes”use Cline\Tracer\Tracer;
test('staged change workflow', function () { $article = Article::create(['title' => 'Original']); $admin = User::factory()->create();
// Stage $staged = $article->stageChanges(['title' => 'New Title']); expect($staged->status)->toBe(StagedChangeStatus::Pending);
// Approve Tracer::approve($staged, $admin); $staged->refresh(); expect($staged->status)->toBe(StagedChangeStatus::Approved);
// Apply $staged->apply($admin); $article->refresh(); expect($article->title)->toBe('New Title');});Mocking Strategies
Section titled “Mocking Strategies”test('uses custom approval strategy', function () { $strategy = Mockery::mock(ApprovalStrategy::class); $strategy->shouldReceive('identifier')->andReturn('mock'); $strategy->shouldReceive('canApprove')->andReturn(true); $strategy->shouldReceive('approve')->andReturn(true);
app()->instance('mock-strategy', $strategy); Tracer::registerApprovalStrategy('mock', 'mock-strategy');
// Test with mocked strategy});Integration Patterns
Section titled “Integration Patterns”With Laravel Nova
Section titled “With Laravel Nova”class Article extends Resource{ public function fields(NovaRequest $request): array { return [ ID::make(), Text::make('Title'),
// Show revision history HasMany::make('Revisions', 'revisions', Revision::class),
// Show pending changes count Number::make('Pending Changes', function () { return $this->pendingStagedChanges()->count(); })->onlyOnDetail(), ]; }
public function actions(NovaRequest $request): array { return [ new ApproveAllStagedChanges(), new RevertToRevision(), ]; }}With API Resources
Section titled “With API Resources”class ArticleResource extends JsonResource{ public function toArray(Request $request): array { return [ 'id' => $this->id, 'title' => $this->title, 'content' => $this->content,
'revisions' => RevisionResource::collection( $this->whenLoaded('revisions') ),
'pending_changes_count' => $this->when( $request->user()?->can('manage', $this->resource), fn() => $this->pendingStagedChanges()->count() ),
'latest_revision' => new RevisionResource( $this->when($request->include_revision, $this->latestRevision()) ), ]; }}With Webhooks
Section titled “With Webhooks”// Notify external systems of changesclass SendWebhookOnRevision{ public function handle(RevisionCreated $event): void { $revision = $event->revision; $model = $revision->traceable;
if (!$model instanceof WebhookEnabled) { return; }
Http::post($model->webhook_url, [ 'event' => 'model.updated', 'model_type' => $model->getMorphClass(), 'model_id' => $model->getKey(), 'version' => $revision->version, 'action' => $revision->action->value, 'changes' => $revision->new_values, 'changed_by' => $revision->causer?->email, 'timestamp' => $revision->created_at->toIso8601String(), ]); }}With Queues
Section titled “With Queues”// Process staged changes asynchronouslyclass ProcessApprovedChanges implements ShouldQueue{ public function handle(): void { StagedChange::query() ->where('status', StagedChangeStatus::Approved) ->where('applied_at', null) ->each(function (StagedChange $staged) { try { $staged->apply(); } catch (CannotApplyStagedChangeException $e) { Log::error("Failed to apply change {$staged->id}", [ 'error' => $e->getMessage(), ]); } }); }}Troubleshooting
Section titled “Troubleshooting”Revisions Not Being Created
Section titled “Revisions Not Being Created”- Check the trait is added:
use HasRevisions - Verify interface implemented:
implements Traceable - Check tracking isn’t disabled:
$model->revisionTrackingEnabled - Ensure attributes aren’t in untracked list
Staged Changes Not Applying
Section titled “Staged Changes Not Applying”- Verify status is
Approved - Check target model still exists
- Verify diff strategy can apply changes
- Check for validation errors on the model
Strategy Not Found
Section titled “Strategy Not Found”// Ensure strategy is registeredTracer::getDiffStrategies(); // List available diff strategiesTracer::getApprovalStrategies(); // List available approval strategiesMemory Issues with Large Diffs
Section titled “Memory Issues with Large Diffs”- Use chunked processing for batch operations
- Consider compact diff strategies for large text
- Prune old revisions regularly
Configuration Reference
Section titled “Configuration Reference”Full config/tracer.php options:
return [ // Primary key type: 'id', 'uuid', 'ulid' 'primary_key_type' => 'id',
// Morph type: 'string', 'uuid', 'ulid' 'morph_type' => 'string',
// Table names 'table_names' => [ 'revisions' => 'revisions', 'staged_changes' => 'staged_changes', 'staged_change_approvals' => 'staged_change_approvals', ],
// Diff strategies 'diff_strategies' => [ 'snapshot' => SnapshotDiffStrategy::class, 'attribute' => AttributeDiffStrategy::class, ], 'default_diff_strategy' => SnapshotDiffStrategy::class,
// Approval strategies 'approval_strategies' => [ 'simple' => SimpleApprovalStrategy::class, 'quorum' => QuorumApprovalStrategy::class, ], 'default_approval_strategy' => SimpleApprovalStrategy::class,
// Quorum settings 'quorum' => [ 'approvals_required' => 2, 'rejections_required' => 1, ],
// Attribute exclusions 'untracked_attributes' => [ 'id', 'created_at', 'updated_at', 'deleted_at', 'remember_token', ], 'unstageable_attributes' => [ 'id', 'created_at', 'updated_at', 'deleted_at', ],
// Morph maps 'morphKeyMap' => [], 'enforceMorphKeyMap' => [],
// Events 'events' => [ 'enabled' => true, ],];