bjthecod3r/laravel-recordables
Laravel Recordables adds simple audit-trail behavior to Eloquent models, helping you track changes, attribute updates, and model events with minimal setup so you can keep a clear history of who changed what and when.
laravel-recordables gives an Eloquent model a change-history trail: instead of hand-rolling an audit table, you attach the package's trait/behaviour and let it persist a record whenever a tracked model's attributes change. Version 0.2.0 is an early-stage release (0.1.0 → 0.2.0), so treat the public API as still settling.
Minimal path to a first working record:
composer require bjthecod3r/laravel-recordablesvendor/bjthecod3r/laravel-recordables/ for the migration stub and config keys before assuming defaults.recordIfChanged, per the 0.2.0 changelog) from the appropriate lifecycle hook — typically saved/updated, or an observer.Where to look first:
boot{TraitName}), because that determines whether you need to call anything manually.Your first real use case should be the boring one: a User or Post model with 2–3 scalar columns, proving the record is written once, with the correct before/after values. Get that right before adding casts, relations, or queues.
Trait-on-model, record-on-save. The default pattern is a trait on the audited model plus a call in the saved (or updated) event. This gives you "one row per save that actually changed something" — which is exactly the behaviour recordIfChanged is named for and what 0.2.0 tightens up.
Observer for cross-model consistency. If you are auditing several models, prefer a shared base observer (or a small abstract model) over copy-pasting the trait call. It keeps the "what counts as changed" logic in one place when the package's comparison rules change — as they did in 0.2.0.
Record the diff, not the whole model. Store changed keys plus old/new values where the package supports it, rather than serialising the full attribute array. Full snapshots balloon fast on wide tables and make "what changed in this edit" queries painful.
Pair with a read-side model. Expose the records through a dedicated Eloquent model with a morphTo back to the audited record. Day-to-day, the useful queries are "history for this record, newest first" and "what did this user change last week" — make sure both indexes exist (morph columns, created_at).
Queue long writes if the package allows it. Audit rows are write amplification on every save. If the config exposes a queue/connection option, use it for high-traffic models; keep it synchronous for models where you need the record inside the same transaction.
Integrate at the boundary, not deep in the domain. Call the recording method from model events or a dedicated service — not sprinkled through controllers. That way saveQuietly(), imports, and console commands behave predictably.
Testing pattern. Use RefreshDatabase, mutate a model, assert a single record exists with the expected changed payload. Add an explicit regression test for cast attributes (see below) so a future package bump can't silently reintroduce duplication.
Casts are the sharp edge. The 0.2.0 fix — "Fix recordIfChanged recording duplicates for cast attributes" — is the single most important thing to know about this release. Eloquent's getDirty() / getOriginal() work on raw (pre-cast) values while $model->attribute returns the cast value; a naive comparison between the two sees a difference where there is none, so an unchanged save produces a spurious record. If you are on 0.1.0 and audit cast attributes (array, json, datetime, AsEnumCollection, custom casts), expect duplicate rows; upgrade, then re-verify existing data if duplicates already landed.
Re-saving without changes should be a no-op. After upgrading, add an assertion that $model->save() twice in a row yields exactly one record. If it yields two, something in your model (a mutator, an accessor with side effects, updated_at touching) is marking attributes dirty.
touch() and timestamp-only saves. touch() fires updated_at changes without real content changes. Decide deliberately whether that is a record worth keeping; usually it is not, so guard against it or use saveQuietly() in those paths.
saveQuietly(), bulk updates, and insert(). Model events don't fire for query-builder updates or bulk inserts, so those changes are invisible to the package. If you rely on the audit trail for compliance, route writes through models or add an explicit bulk-recording path.
Watch the transaction boundary. If recording is synchronous, a failing record write can roll back your domain save (or vice versa). Know which side you want to win; wrap deliberately in DB::transaction() rather than relying on default behaviour.
Pre-1.0 API churn. At 0.2.0 the package has no stability guarantee. Pin the version in composer.json, read the changelog before each bump, and keep the trait call isolated behind one observer/service so a signature change is a one-file edit.
Debugging duplicated or missing records. Log inside the model event: dump $model->getDirty(), $model->getChanges(), and $model->getCasts() for the attributes in question. That trio usually identifies cast mismatches immediately. Also confirm the trait's boot method is actually running (a typo'd trait name fails silently — no records, no error).
Extension points to prefer. Config-driven table/model names, a custom record model extending the package's, and overriding the "should record" predicate are the stable seams. Avoid overriding the diff-building internals, which is precisely the code 0.2.0 changed.
How can I help you explore Laravel packages today?