luissantiago/elogquent
Elogquent is a Laravel package for logging Eloquent model events. Add its trait to models to automatically record create, update, and delete actions—including changed attributes—for simple auditing and activity history.
composer require luissantiago/elogquent
php artisan vendor:publish --provider="Luissantiago\Elogquent\ElogquentServiceProvider"
php artisan migrate
The vendor:publish step is the most important one — it should give you both a config file (typically config/elogquent.php) and a migration that creates the history table. Read those two files before touching any model code; they define the contract for everything else (table name, column layout, which attributes are tracked by default, and whether an actor/user id is captured).
Next, look at the package's trait (usually something like Luissantiago\Elogquent\Traits\HasHistory or LogsChanges). That trait is what hooks into Eloquent's created / updated / deleted events. Add it to a single model first — a User or Order is a good candidate — and explicitly declare the attributes you want recorded:
use Luissantiago\Elogquent\Traits\LogsChanges;
class Order extends Model
{
use LogsChanges;
protected $logAttributes = ['status', 'total', 'assigned_to'];
}
First use case: an audit trail on Order::status. Update an order, then query the history table (or the model's history relation) and confirm you see one row per changed attribute with old and new values. If that works, you understand 90% of the package.
Trait-per-model, not global. Apply the trait only to models that genuinely need auditing. Blanket-applying it via a base model tends to produce noisy history on join tables, pivot-ish models, and session/token models.
Config defaults vs. per-model overrides. The published config generally holds global defaults (table name, guarded attributes, whether to record the acting user). Per-model properties like $logAttributes / $logExcept should override those. Treat the config as the floor and the model property as the specific contract — reviewers should be able to open a model and see exactly what is audited without cross-referencing config.
Actor context. Audit rows are far more useful with a user_id. Most implementations resolve this from auth()->id() automatically; if yours doesn't, set it explicitly in a service or middleware, or wrap writes in a small helper that stamps the current user onto the log entries. This is the single highest-value integration you'll make.
Querying history. Rather than hitting the history table raw, expose it through a relation on the model:
$order->history()->latest()->get();
and wrap common reads in scopes — forAttribute('status'), between($from, $to), byUser($id). A thin repository or query object around the history model keeps controllers clean and makes it trivial to change the storage layout later.
Service-layer writes. Because the logging rides on Eloquent model events, all writes must go through the model. Any Order::where(...)->update([...]) in a service class will silently bypass logging. Establish a convention early: services call $order->update(...), never the query builder. Code review should flag mass updates on audited models.
Bulk imports and jobs. For seeders, imports, and data migrations you usually don't want thousands of history rows. Use saveQuietly() / updateQuietly(), or Model::withoutEvents(fn () => ...), and note in the code why logging is suppressed. The reverse applies in queued jobs — if logging is queued, make sure the job runs after the transaction commits.
Timeline UI. A common real-world use is an activity feed on a record's detail page. Map attribute names to human labels in a presenter (status → "Status"), format casts (dates, money, enums), and render old → new. Keep that formatting out of the history model itself so the raw data stays clean.
Model::where(...)->update(), insert(), and raw SQL do not fire model events. This is the #1 source of "why is my history empty?" bugs. Grep for ->update( on audited models during review.saveQuietly / updateQuietly / withoutEvents silently skip logging. Great for tests and imports, dangerous if copied into production paths. Add a comment whenever you use them.password, remember_token, API keys, and anything encrypted from the tracked attributes. History tables are usually read by more people (and more admin panels) than the source table. Check the config's default exclusion list first.text columns and JSON-cast attributes can be truncated or double-encoded. Verify how arrays, enums, and Carbon instances round-trip — and cast them back on read.config/elogquent.php, run php artisan config:clear. In CI this bites regularly.phpunit.xml or a base test case for tests that don't care about history, and use RefreshDatabase so the history table is migrated. For tests that do assert on history, assert on the specific attribute row rather than row counts — counts are brittle.last_seen_at) will bloat the table fast. Exclude those columns, and add an index on (model_type, model_id, created_at) if the package's migration doesn't already.How can I help you explore Laravel packages today?