ModelScribe sits between your Eloquent models and your persistence layer. When a model changes, an observer intercepts the lifecycle event, assembles an immutableDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/HypathStack/model-scribe/llms.txt
Use this file to discover all available pages before exploring further.
LogEntry value object, and hands it off to the DriverManager, which routes it to whatever backend you have configured — a database table, a log file, or both at once via the stack driver. The only thing you need to do is add the HasAuditLog trait to a model; everything else is wired up automatically.
The Audit Pipeline
Eloquent fires a lifecycle event
Laravel’s Eloquent ORM dispatches events like
created, updated, deleted, and restored any time a model’s state changes. ModelScribe listens for all four.ModelScribeObserver intercepts the event
Every model using the
HasAuditLog trait is registered with ModelScribeObserver at boot time. The observer’s corresponding method (created(), updated(), etc.) calls the internal handle() method, which first verifies the event is in the model’s $auditEvents list before proceeding.Observer builds the LogEntry DTO
handle() calls buildProperties() to produce the before/after diff, then calls resolveCauser() to identify the authenticated user. It combines these with request context (URL, IP, User Agent) and model metadata (log name, tags, driver preference) into a new LogEntry instance.Log name is resolved
resolveLogName() determines the final routing key. If the model has a custom $auditLogName set, that value is used directly. If the log name is still default, the observer checks guard_stores in the config and uses the first matching guard’s mapped log name.DriverManager resolves the driver
The observer calls
$this->manager->driver($driverName), where $driverName comes from the model’s $auditDriver property (or the global default driver from config). The DriverManager resolves the driver once and caches it for the lifetime of the request.Driver persists the entry
The resolved driver’s
log(LogEntry $entry) method is called. The database driver inserts a row (or multiple rows, for multi-table stores). The file driver writes a structured info-level log line. The stack driver fans the entry out to each of its listed sub-drivers in sequence.Observer Registration via bootHasAuditLog()
When Laravel boots an Eloquent model, it automatically calls any static boot{TraitName}() method defined in a trait. HasAuditLog uses this hook to register the observer with zero boilerplate:
use HasAuditLog; to a model, the observer is active — no service provider registration, no manual ::observe() call needed.
How buildProperties() Constructs the Diff
The properties array on every LogEntry always contains two keys: old and attributes. What goes into each key depends on the event type.
created and restored
Both events represent a model coming into existence (or back into existence). There is no meaningful “before” state, so old is always an empty array. All current attributes — or the subset defined in $auditAttributes for the event — are captured under attributes.
updated
Only the attributes that actually changed are recorded. The observer calls $model->getDirty() to find changed keys, then fetches the pre-change values via $model->getOriginal($key). If $auditAttributes limits the tracked fields for the updated event, any dirty key not in that list is silently skipped.
deleted
A deletion has no meaningful “after” state. The current attribute snapshot is placed in old, and attributes is an empty array. This mirrors the created shape in reverse, making it easy to identify tombstone records at a glance.
How resolveLogName() Picks a Routing Key
The final log name written to the LogEntry is determined by this two-step fallback:
-
Model-defined name wins. If
$model->getAuditLogName()returns anything other than'default', that value is used as-is. This is the cleanest way to route a specific model to its own table or log store. -
Guard-store mapping as fallback. If the name is still
'default', the observer iterates over theguard_storesarray inconfig/model-scribe.php. The first guard that has an authenticated user becomes the source of the mapped log name. This lets you automatically route API-authenticated writes to a different store than web-authenticated ones — without touching the model.
How resolveCauser() Identifies the Actor
The observer resolves the causer by checking the auth_guard setting in config/model-scribe.php:
- If
auth_guardisnull(the default), it callsAuth::user()and uses whichever guard is currently active. - If
auth_guardnames a specific guard (e.g.,'api'), it callsAuth::guard('api')->user()regardless of the default guard.
Model instance to be stored; any non-model value (such as a custom user object) is treated as null.
When
capture_request_context is true in your config (the default), the observer automatically attaches the full request URL (Request::fullUrl()), the client IP address (Request::ip()), and the User Agent string (Request::userAgent()) to every LogEntry. Set capture_request_context to false to suppress this for queue workers or console commands where HTTP context is unavailable.