Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Model History Laravel Package

audunru/model-history

A Laravel package that keeps a history of changes to your Eloquent models. Automatically records creates, updates, and deletes so you can see who changed what and when, browse past versions, and build audit trails with a simple, lightweight API.

View on GitHub
Deep Wiki
Context7

Getting Started

The package adds an audit trail to Eloquent models: install it, publish its config and migration, run the migration, then add the history trait to any model you want tracked.

composer require audunru/model-history
php artisan vendor:publish --tag=model-history-config   # check `php artisan vendor:publish` for the exact tag in your version
php artisan vendor:publish --tag=model-history-migrations
php artisan migrate
use AudunRu\ModelHistory\Traits\HasHistory;

class Order extends Model
{
    use HasHistory;
}

First places to look after installing:

  1. The published config (config/model-history.php) — user/causer model, table name, enabled flag, ignored attributes.
  2. The published migration — confirms the actual table and column names (owner_type/owner_id, user_type/user_id, and the JSON payload column) before you write queries against them.
  3. The History model shipped by the package — its owner morphTo and user relations are what you will render and eager load.
  4. AppServiceProvider — if you use Relation::enforceMorphMap(), verify it here, because v4.1.4 changes what gets written to owner_type.

First real use case: an Order/Invoice detail page with a "History" timeline showing who changed what and when. Create an order, update a field, then inspect the row directly (php artisan tinker) before building UI on top of it — that five-minute check tells you exactly what the payload looks like in your app.

Implementation Patterns

Trait-per-model, read through the relation. Add the trait only to models that need an audit trail (not a base model you inherit everywhere) — otherwise you accumulate high-volume, low-value rows. Read history through the relation and always paginate:

$order->histories()->with('user')->latest()->paginate(25);

Render diffs, not snapshots. The JSON payload holds the changed attributes. Map them to human labels via $model->getAttributeLabel()-style helpers or a translation file, and render only keys present in the diff so the timeline stays readable as the schema evolves.

Causer resolution. The package resolves the acting user from the auth guard. For queued jobs, console commands, seeders, and impersonation, the guard is empty or wrong — set the causer explicitly (via the package's user resolver/config hook, or by writing the history record yourself in those paths). Decide up front whether "system" changes should appear as null user or a dedicated service account; retrofitting this later means backfilling rows.

Morph maps are a contract. Register them once, early, and never rename an alias:

// AppServiceProvider::boot()
Relation::enforceMorphMap([
    'order'   => \App\Models\Order::class,
    'invoice' => \App\Models\Invoice::class,
]);

v4.1.4 makes history store the alias rather than the FQCN, so owner_type becomes order, not App\Models\Order. New installs get this for free; existing installs need a backfill (see Gotchas).

Transaction-aware writes. History rows are written during the model event, so they participate in your surrounding transaction. That's usually what you want (rollback discards history too), but it means a long-running transaction holds extra inserts — watch this on bulk import jobs and consider saveQuietly() plus an explicit, batched history write for those.

Exposure layer. Wrap history in a controller + policy rather than exposing the relation directly through an API resource. History leaks field names, old values, and user identities; gate it per-resource (viewHistory) and redact sensitive diffs before serialization.

Retention. Add a scheduled prune (histories()->where('created_at', '<', now()->subYear())->delete() in chunks) from day one. Audit tables only grow.

Gotchas and Tips

  • v4.1.4 changes stored data, not the API. Upgrading is non-breaking at the code level, but any history written before the upgrade has the FQCN in owner_type while new rows have the morph alias. That mixed state breaks where('owner_type', 'order') filters and can throw under Relation::enforceMorphMap() when a morphTo tries to resolve a stale FQCN. Backfill immediately after deploying:

    DB::table('histories')
        ->where('owner_type', Order::class)
        ->update(['owner_type' => 'order']);
    

    Run it per mapped alias, on a copy first, and include it in the same deploy as the upgrade so no writes land in between.

  • Aliases are permanent schema. Renaming an alias later silently orphanates every historical row pointing at the old name. Treat the morph map like a migration: additive only.

  • Silent no-ops. Model::where(...)->update(), ->delete(), insert(), raw SQL, saveQuietly(), and withoutEvents() all bypass model events — no history. If a report "the change wasn't logged," check the write path first, then the trait, then the enabled flag in config.

  • Tests: Event::fake() in a feature test will suppress history writes and produce confusing assertions. Assert on history only in tests that don't fake events, and use RefreshDatabase with the published migration so the table exists.

  • Sensitive attributes. Password hashes, API tokens, and encrypted casts can end up in the diff payload. Exclude them via the package's ignore configuration or a model-level override, and audit the payload of an update to a sensitive model before shipping.

  • Large payloads. Long text columns and JSON blobs bloat the table quickly. Exclude them if you only need to know that a field changed, not its full before/after.

  • N+1 on index pages. A history tab per row is a classic N+1 — eager load histories.user or load counts, and paginate server-side.

  • Null causer everywhere. Seeded data, scheduled commands, and queue workers produce rows with no user. Make the UI label these explicitly ("System") so they don't look like corruption.

  • Extension points worth knowing: the configurable user model, the table/connection settings, a custom History model if you need extra columns or scopes, and the model-level hook for customizing which attributes are recorded. Prefer these over forking the trait.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment