LaravelAuditingPHPSecurityActivity Log

Laravel Auditing Package — Track Model Changes the Right Way

The Laravel auditing package records old and new model values, the user who changed them, and when it happened—without you inventing a second history table for every feature.

Laravel Auditing Package — Track Model Changes the Right Way

Quick Answer

The Laravel auditing package (owen-it/laravel-auditing) stores a history of Eloquent model changes: old values, new values, event type, user, IP, and timestamp. You add the Auditable trait, run a migration, and create/update/delete events write rows to an audits table. Use it for compliance trails and “who changed this?” — not as a generic application event bus.

Quick Facts

Topic: Laravel Auditing Package
Category: Backend / Laravel Packages

Table of Contents

  • What Is Laravel Auditing Package?
  • Why It Matters
  • How It Works
  • Step-by-Step Guide
  • Real Example
  • Pros & Cons
  • Best Practices
  • Common Mistakes
  • FAQs
  • Key Takeaways

What Is Laravel Auditing Package?

When people say “Laravel audit package,” they usually mean owen-it/laravel-auditing. It watches Eloquent models and writes an audit row whenever a record is created, updated, deleted, or restored.

Each audit is a morph to the model (auditable_type + auditable_id) plus a morph to the user who did it (user_type + user_id). Old and new attributes live in JSON columns. You can later list history on a document, CAPA, or user profile without a custom *_histories table per module.

It is not the same as Spatie’s activity log. Activity log is a general “something happened” feed. Auditing is focused on attribute-level diffs on models. In regulated Laravel apps (the kind of Document Control / CAPA work on this stack), you often want the diff, not just a sentence in a log.

Why It Matters

  • Who changed what — support and QA can answer that without reading MySQL binlogs.
  • Compliance-friendly trail — create/update/delete with actor and timestamp is the minimum many quality systems expect.
  • Less custom history code — one audits table instead of copy-pasted observers in every module.
  • Debugging data bugs — a bad mass-assignment or import shows up as a sudden old→new jump you can replay.

How It Works

Auditing hooks Eloquent events. After the model saves, the package builds an audit from the dirty attributes (or the full snapshot, depending on config) and inserts it.

  1. Model is auditable — it uses OwenIt\Auditing\Auditable and implements Auditable.
  2. Event firescreated, updated, deleted, restored (you choose which in config or per model).
  3. Resolver data — user id, IP, user agent, URL come from resolvers. In HTTP that’s the logged-in user. In queues/artisan you must set a resolver or accept user_id = null.
  4. Audit rowold_values / new_values JSON, event, morph keys, optional tags.
  5. Read path$model->audits or a query on the Audit model for reports.

Global scopes and saveQuietly() matter. Quiet saves skip Eloquent events, so they skip audits. That’s a feature when you sync a denormalized counter; it’s a hole if you “fix data” in tinker with saveQuietly() on a controlled document.

Step-by-Step Guide

Package APIs shift across major versions. Match the install to your Laravel version, then keep the same idea: trait + migration + exclude secrets.

Step 1: Install and migrate

composer require owen-it/laravel-auditing
php artisan vendor:publish --provider "OwenIt\Auditing\AuditingServiceProvider" --tag="config"
php artisan vendor:publish --provider "OwenIt\Auditing\AuditingServiceProvider" --tag="migrations"
php artisan migrate

Confirm an audits table exists. Don’t hand-edit that schema unless you know you need extra columns (tenant id, plant id, etc.).

Step 2: Make a model auditable

use OwenIt\Auditing\Contracts\Auditable;
use OwenIt\Auditing\Auditable as AuditableTrait;

class Document extends Model implements Auditable
{
    use AuditableTrait;

    protected $fillable = ['title', 'status', 'owner_id', 'content'];
}

Repeat only on models that need a trail. Auditing every sessions-style or cache table is noise and disk.

Step 3: Exclude secrets and useless columns

In the model or in config/audit.php:

// on the model
protected $auditExclude = [
    'remember_token',
    'password',
    'updated_at', // optional; some teams keep it
];

public function transformAudit(array $data): array
{
    unset($data['new_values']['password'], $data['old_values']['password']);
    return $data;
}

Never audit password hashes, API tokens, or 2FA secrets. If a column is huge (long HTML), either exclude it or store a hash/summary — JSON blobs will bloat audits fast.

Step 4: Control which events you keep

// config/audit.php (concept)
'events' => [
    'created',
    'updated',
    'deleted',
    'restored',
],

Per-model override if a lookup table only needs updated. Skip retrieved unless you have a real “who viewed this” requirement — that’s volume, not an audit of changes.

Step 5: Show history in the UI

$document = Document::with('audits.user')->findOrFail($id);

foreach ($document->audits as $audit) {
    // $audit->event, $audit->user, $audit->old_values, $audit->new_values, $audit->created_at
}

Render a simple timeline: actor, event, changed keys. Don’t dump raw JSON to end users in a pharma/QMS screen — map keys to labels (status → Status).

Step 6: Handle console, jobs, and impersonation

HTTP requests get auth()->user() for free. Seeders, imports, and queue jobs do not. Set the auditor explicitly or accept system rows:

// concept — use the API your installed version documents
Document::disableAuditing();
// bulk import
Document::enableAuditing();

If admins impersonate users, make sure the resolver stores the real actor (or both actor and impersonator). Otherwise the trail lies.

Step 7: Prune or archive

Audits grow forever. Add a scheduled prune for noisy models, or archive old rows to cold storage. Keep longer retention on regulated entities (documents, CAPA) than on Last seen at style fields.

Real-World Example

A Document Control module had a custom document_revisions table that only stored “new file uploaded.” Status flips, owner changes, and “who un-obsolete’d this?” were missing. QA failed a trail question in an audit meeting.

After the auditing package: Document and DocumentVersion used the Auditable trait. Status, owner, and effective dates showed old→new. The file upload still wrote a revision row (binary files don’t belong in audits JSON). Reviewers opened a History tab: user, timestamp, fields.

The remaining gap was an artisan backfill that used saveQuietly() to set legacy_id. Those updates left no audit. The fix was a tagged audit or a one-off “system migration” note — not more observers. Same lesson as the Laravel lifecycle: if Eloquent never fires updated, no package can see it.

Pros & Cons

Advantages

  • Attribute-level old/new values on Eloquent models
  • One morph table instead of per-module history schemas
  • User, IP, and URL resolvers included
  • Works with restore on SoftDeletes if you enable that event

Disadvantages

  • JSON history is awkward to query (“all documents whose status became Obsolete last month” needs JSON functions or extra indexes)
  • High-churn models will grow the table quickly
  • Does not replace a full event-sourced domain or file version storage
  • Queue/console actors are easy to get wrong (user_id null)

Best Practices

  • Audit business entities, not every Eloquent model.
  • Exclude passwords, tokens, and giant HTML from old_values / new_values.
  • Index auditable_type, auditable_id, user_id, created_at.
  • Don’t use saveQuietly() on records you legally need to trail.
  • Keep file binaries in your own revision table; audit metadata only.
  • Decide retention per model type before production data explodes.
  • Pair with policies — auditing records what happened; it does not block unauthorized writes. Auth still belongs in gates/policies and API auth.

Common Mistakes

  • Auditing the User password field → Fix: exclude it; never put hashes in JSON logs.
  • Expecting audits from Model::query()->update() → Fix: mass updates skip model events. Loop models or write an explicit audit.
  • Logging retrieved/view events like changes → Fix: use a separate view log; don’t drown audits.
  • No user on queued jobs → Fix: pass actor id into the job and set it in a resolver, or mark event as system.
  • Showing raw JSON to auditors → Fix: human labels and only changed keys.
  • One giant unindexed audits table → Fix: indexes + prune/archive.

Frequently Asked Questions

What is the Laravel auditing package?

It is an Eloquent auditing library (commonly owen-it/laravel-auditing) that stores who changed a model, which attributes changed, and when. History lives in an audits table related by morphs.

How do I set up Laravel auditing?

Composer require the package, publish config and migrations, migrate, then implement Auditable and use the trait on each model you care about. Exclude secrets, then read $model->audits in the UI.

Laravel auditing vs Spatie activity log?

Auditing stores old/new attribute diffs on a model. Spatie activity log stores a description of an activity, optionally with properties. Use auditing for field-level trails; use activity log for “User X approved CAPA Y” style events. Many apps use both.

Is the Laravel auditing package worth it?

Yes if you keep answering “who changed this row?” or you need a basic compliance trail on Eloquent data. Skip it for throwaway CRUD with no accountability, or if you already have a mature event-sourcing store.

Does Laravel auditing work with SoftDeletes?

Yes if you enable deleted and restored events. A force delete may still need an extra policy so you don’t wipe the only copy of history by cascading the audits table without a backup.

Summary

The Laravel auditing package gives you a single, morph-based history of Eloquent changes: old values, new values, actor, and event. Install it, trait the models that matter, and keep secrets out of JSON.

It will not see mass update() queries or saveQuietly(). Plan resolvers for jobs, indexes for the audits table, and retention before the table outgrows the feature.

Next: pick one critical model (Document, Order, User role assignment), enable audits, and ship a History tab. For Laravel + Nuxt product work, get in touch.

Key Takeaways

  • Audit package = Eloquent field diffs in an audits table, not a generic logger.
  • Trait only business models; exclude passwords and huge columns.
  • Mass updates and quiet saves skip audits because they skip model events.
  • Index morph columns and prune high-churn data.
  • Use Spatie activity log for human events; use auditing for attribute history — they solve different jobs.

Comments

0 comments · new ones appear after approval

No comments yet. Be the first to share your thoughts.

Leave a comment

Your comment will be reviewed before it appears.