Skip to main content

Auditing Eloquent Models

Chronicle provides two opt-in mechanisms for automatically recording audit entries when Eloquent models change: the HasChronicle trait (for your own models) and ChronicleModelObserver (for third-party models you cannot modify).

Both are explicit opt-in — Chronicle never attaches global observers automatically.

HasChronicle trait

Add the trait to any Eloquent model you want to audit:

use Chronicle\Eloquent\HasChronicle;

class Order extends Model
{
use HasChronicle;
}

Chronicle will record entries on created, updated, and deleted model events. For updated, it skips entries when the only changes are timestamp fields.

Default behaviour

EventAction recorded
createdorder.created
updatedorder.updated (with diff, if changes exist)
deletedorder.deleted

The action prefix defaults to snake_case of the model's base class name (e.g. OrderItemorder_item).

The actor defaults to Auth::user() ?? 'system'.

Customising recorded events

Override $chronicleEvents to limit which lifecycle events are audited:

class Order extends Model
{
use HasChronicle;

protected array $chronicleEvents = ['created', 'deleted']; // skip 'updated'
}

Ignoring specific fields in diffs

Override $chronicleIgnore to exclude fields from the updated diff:

class Order extends Model
{
use HasChronicle;

protected array $chronicleIgnore = ['last_synced_at', 'cache_key'];
}

created_at and updated_at are always excluded by default.

Overridable methods

Override these protected methods in your model for finer control:

class Order extends Model
{
use HasChronicle;

// Change the actor resolution
protected function chronicleActor(): mixed
{
return $this->assignedUser ?? Auth::user() ?? 'system';
}

// Change the action prefix
protected function chronicleActionPrefix(): string
{
return 'shop.order'; // produces: shop.order.created, etc.
}

// Change which fields are ignored in diffs (merged with defaults)
protected function chronicleIgnoredFields(): array
{
return array_merge(parent::chronicleIgnoredFields(), ['search_vector']);
}
}

Chronicle::observe() for third-party models

When you cannot add HasChronicle to a model directly (e.g. a package model), register a ChronicleModelObserver:

// Register in a ServiceProvider
use Chronicle\Facades\Chronicle;

// Use the default observer
Chronicle::observe(Invoice::class);

// Use a custom observer subclass
Chronicle::observe(Invoice::class, InvoiceObserver::class);

Writing a custom observer

Extend ChronicleModelObserver and override any protected method:

use Chronicle\Eloquent\ChronicleModelObserver;
use Illuminate\Database\Eloquent\Model;

class InvoiceObserver extends ChronicleModelObserver
{
// Restrict to specific events
protected function recordedEvents(): array
{
return ['created', 'updated'];
}

// Custom actor resolution
protected function resolveActor(Model $model): Model|string
{
return Auth::user() ?? 'billing-system';
}

// Custom action prefix
protected function actionPrefix(Model $model): string
{
return 'billing.invoice';
}

// Extra fields to exclude from diffs (merged with timestamps)
protected array $ignoredFields = ['stripe_metadata', 'pdf_cache'];
}

The base ChronicleModelObserver records created, updated, and deleted by default, derives the action prefix from snake_case of the model class name, and uses Auth::user() ?? 'system' as the actor.

Low magic

Chronicle's Eloquent integration is deliberately explicit. You must opt each model in individually. There are no global hooks, no automatic activity recording, and no framework-level interception. Every audit entry that Chronicle records is the result of a deliberate developer decision — either a direct Chronicle::record() call or an explicit observer registration.

See also