Staged Changes
This guide covers the staged changes system, which allows you to queue model changes for review and approval before they’re persisted.
When to Use Staged Changes
Section titled “When to Use Staged Changes”Staged changes are ideal for:
- Content moderation: Review user-submitted content before publishing
- Maker-checker workflows: Require approval for sensitive data changes
- Compliance: Audit trail of proposed vs. approved changes
- Collaborative editing: Multiple reviewers for important updates
Setting Up a Model
Section titled “Setting Up a Model”Add the HasStagedChanges trait and implement the Stageable interface:
use Cline\Tracer\Concerns\HasStagedChanges;use Cline\Tracer\Contracts\Stageable;use Illuminate\Database\Eloquent\Model;
class Article extends Model implements Stageable{ use HasStagedChanges;
protected $fillable = ['title', 'content', 'status'];}For both revision tracking and staged changes:
use Cline\Tracer\Concerns\HasRevisions;use Cline\Tracer\Concerns\HasStagedChanges;use Cline\Tracer\Contracts\Stageable;use Cline\Tracer\Contracts\Traceable;
class Article extends Model implements Traceable, Stageable{ use HasRevisions; use HasStagedChanges;}Staging Changes
Section titled “Staging Changes”All staging operations use the Tracer facade:
Basic Staging
Section titled “Basic Staging”use Cline\Tracer\Tracer;
$article = Article::find(1);
$stagedChange = Tracer::staging($article)->stage([ 'title' => 'Updated Title', 'content' => 'Updated content here...',]);With a Reason
Section titled “With a Reason”$stagedChange = Tracer::staging($article)->stage( ['title' => 'Fixed Typo in Title'], 'Correcting spelling mistake reported by user');Via TracerManager Directly
Section titled “Via TracerManager Directly”use Cline\Tracer\Tracer;
$stagedChange = Tracer::stage($article, [ 'title' => 'New Title',], 'Marketing requested title change');Staged Change Lifecycle
Section titled “Staged Change Lifecycle”┌─────────────────────────────────────────────────────────────────┐│ STAGED CHANGE │├─────────────────────────────────────────────────────────────────┤│ ││ ┌─────────┐ ┌──────────┐ ┌─────────┐ ││ │ Pending │────▶│ Approved │────▶│ Applied │ ││ └────┬────┘ └──────────┘ └─────────┘ ││ │ ││ │ ┌──────────┐ ││ ├─────────▶│ Rejected │ ││ │ └──────────┘ ││ │ ││ │ ┌───────────┐ ││ └─────────▶│ Cancelled │ ││ └───────────┘ ││ │└─────────────────────────────────────────────────────────────────┘| Status | Description |
|---|---|
pending | Awaiting approval (mutable) |
approved | Ready to apply (can be applied) |
rejected | Denied (terminal) |
applied | Changes persisted to model (terminal) |
cancelled | Withdrawn (terminal) |
Approving and Rejecting
Section titled “Approving and Rejecting”Simple Approval
Section titled “Simple Approval”use Cline\Tracer\Tracer;
// ApproveTracer::approve($stagedChange, auth()->user(), 'Looks good!');
// RejectTracer::reject($stagedChange, auth()->user(), 'Content violates guidelines');Via Conductor
Section titled “Via Conductor”$staging = Tracer::staging($article);
// Get pending changes$pending = $staging->pending();
// Approve specific change$staging->approve($stagedChange, $approver, 'Approved for publication');
// Reject with reason$staging->reject($stagedChange, $rejector, 'Needs more detail');Applying Changes
Section titled “Applying Changes”Once approved, changes must be explicitly applied:
Apply Single Change
Section titled “Apply Single Change”$stagedChange->apply(auth()->user());Apply All Approved Changes
Section titled “Apply All Approved Changes”use Cline\Tracer\Tracer;
$appliedCount = Tracer::staging($article)->applyApproved(auth()->user());// Returns number of staged changes appliedVia TracerManager
Section titled “Via TracerManager”Tracer::apply($stagedChange, auth()->user());Querying Staged Changes
Section titled “Querying Staged Changes”Via Relationships (read-only)
Section titled “Via Relationships (read-only)”// All staged changes via relationship$all = $article->stagedChanges;
// Pending only via relationship$pending = $article->pendingStagedChanges()->get();
// Approved only via relationship$approved = $article->approvedStagedChanges()->get();Via Conductor (recommended)
Section titled “Via Conductor (recommended)”use Cline\Tracer\Tracer;
$staging = Tracer::staging($article);
// Check for pendingif ($staging->hasPending()) { // Show "pending review" indicator}
// Check for approvedif ($staging->hasApproved()) { // Show "ready to apply" indicator}Global Queries
Section titled “Global Queries”use Cline\Tracer\Tracer;
// All pending changes across all models$allPending = Tracer::allPendingStagedChanges();
// All approved changes ready to apply$allApproved = Tracer::allApprovedStagedChanges();Via Conductor
Section titled “Via Conductor”$staging = Tracer::staging($article);
$pending = $staging->pending();$approved = $staging->approved();$all = $staging->all();Working with Staged Change Data
Section titled “Working with Staged Change Data”Access Proposed Values
Section titled “Access Proposed Values”$stagedChange->proposed_values;// ['title' => 'New Title', 'content' => 'New content']
$stagedChange->getProposedValue('title');// 'New Title'
$stagedChange->getProposedValue('missing_key', 'default');// 'default'Access Original Values
Section titled “Access Original Values”$stagedChange->original_values;// ['title' => 'Old Title', 'content' => 'Old content']
$stagedChange->getOriginalValue('title');// 'Old Title'Check What Would Change
Section titled “Check What Would Change”$stagedChange->getChangedAttributeKeys();// ['title', 'content']
$stagedChange->wouldChange('title'); // true$stagedChange->wouldChange('status'); // falseGet Human-Readable Descriptions
Section titled “Get Human-Readable Descriptions”use Cline\Tracer\Tracer;
$diffStrategy = Tracer::resolveDiffStrategy($stagedChange->diff_strategy);$descriptions = $diffStrategy->describe($stagedChange->proposed_values);// [// 'title' => 'Changed from "Old Title" to "New Title"',// 'content' => 'Changed from "..." to "..."',// ]Modifying Staged Changes
Section titled “Modifying Staged Changes”Update Proposed Values
Section titled “Update Proposed Values”Only pending changes can be modified:
$stagedChange->updateProposedValues([ 'title' => 'Even Newer Title',]);This merges with existing proposed values.
Cancel a Staged Change
Section titled “Cancel a Staged Change”$stagedChange->cancel();Cancel All Pending
Section titled “Cancel All Pending”use Cline\Tracer\Tracer;
$cancelled = Tracer::staging($article)->cancelPending();// Returns number cancelledControlling Stageable Attributes
Section titled “Controlling Stageable Attributes”Configuration is managed via config/tracer.php or runtime registration, not on the model itself.
Via Config File
Section titled “Via Config File”'models' => [ App\Models\Article::class => [ 'stageable_attributes' => ['title', 'content'], 'unstageable_attributes' => ['internal_notes', 'admin_only'], ],],Via Runtime Registration
Section titled “Via Runtime Registration”use Cline\Tracer\Tracer;
// Only allow staging specific attributesTracer::configure(Article::class) ->stageableAttributes(['title', 'content']);
// Exclude specific attributes from stagingTracer::configure(Article::class) ->unstageableAttributes(['internal_notes', 'admin_only']);Global Unstageable Attributes
Section titled “Global Unstageable Attributes”Configure in config/tracer.php:
'unstageable_attributes' => [ 'id', 'created_at', 'updated_at', 'deleted_at',],Author Resolution
Section titled “Author Resolution”By default, the authenticated user is recorded as the author via the configured CauserResolver:
use Cline\Tracer\Tracer;
$stagedChange = Tracer::staging($article)->stage(['title' => 'New']);$stagedChange->author; // The authenticated userCustom Author Resolution
Section titled “Custom Author Resolution”Create a custom CauserResolver to customize how authors are resolved:
use Cline\Tracer\Contracts\CauserResolver;use Illuminate\Database\Eloquent\Model;
class CustomCauserResolver implements CauserResolver{ public function resolve(): ?Model { // Use API client for API requests if (request()->hasHeader('X-API-Key')) { return ApiClient::findByKey(request()->header('X-API-Key')); }
return auth()->user(); }}Register in config/tracer.php:
'causer_resolver' => CustomCauserResolver::class,Approval Metadata
Section titled “Approval Metadata”Each staged change tracks approval workflow data:
$stagedChange->approval_metadata;// For simple approval:// [// 'approved_by_type' => 'App\\Models\\User',// 'approved_by_id' => 5,// 'approved_at' => '2024-01-15T10:30:00+00:00',// ]
// For quorum approval:// [// 'quorum_reached' => true,// 'approvals_required' => 2,// 'approvals_received' => 2,// 'approved_at' => '2024-01-15T10:30:00+00:00',// ]Approval Records
Section titled “Approval Records”Each approval/rejection is recorded individually:
$stagedChange->approvals;// Collection of StagedChangeApproval models
foreach ($stagedChange->approvals as $approval) { $approval->approver; // User model $approval->approved; // true/false $approval->comment; // Approval/rejection comment $approval->sequence; // Order of approval $approval->created_at; // When they voted}Events
Section titled “Events”Tracer dispatches events throughout the staged change lifecycle:
use Cline\Tracer\Events\StagedChangeCreated;use Cline\Tracer\Events\StagedChangeApproved;use Cline\Tracer\Events\StagedChangeRejected;use Cline\Tracer\Events\StagedChangeApplied;
// In EventServiceProviderprotected $listen = [ StagedChangeCreated::class => [ NotifyReviewersListener::class, ], StagedChangeApproved::class => [ NotifyAuthorApprovedListener::class, ], StagedChangeRejected::class => [ NotifyAuthorRejectedListener::class, ], StagedChangeApplied::class => [ LogChangeAppliedListener::class, ],];Error Handling
Section titled “Error Handling”Cannot Modify Non-Mutable Changes
Section titled “Cannot Modify Non-Mutable Changes”use Cline\Tracer\Exceptions\CannotModifyStagedChangeException;
try { $approvedChange->updateProposedValues(['title' => 'New']);} catch (CannotModifyStagedChangeException $e) { // Change is already approved/rejected/applied}Cannot Apply Non-Approved Changes
Section titled “Cannot Apply Non-Approved Changes”use Cline\Tracer\Exceptions\CannotApplyStagedChangeException;
try { $pendingChange->apply();} catch (CannotApplyStagedChangeException $e) { // Change must be approved first}Target Model Not Found
Section titled “Target Model Not Found”try { $stagedChange->apply();} catch (CannotApplyStagedChangeException $e) { // The target model was deleted}Integration with Revisions
Section titled “Integration with Revisions”When using both traits, applying a staged change creates a revision:
class Article extends Model implements Traceable, Stageable{ use HasRevisions; use HasStagedChanges;}
// Stage a change$staged = Tracer::staging($article)->stage(['title' => 'New Title']);
// Approve and applyTracer::approve($staged, $admin);$staged->apply($admin);
// A revision is automatically created$revision = $article->latestRevision();$revision->action; // RevisionAction::Updated$revision->new_values; // ['title' => 'New Title']Next Steps
Section titled “Next Steps”- Approval Workflows - Configure approval strategies
- Strategies - Customize diff calculation
- Advanced Usage - Events, custom strategies, and more