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

Laravel Audit Laravel Package

jarek-paprocki/laravel-audit

Laravel Audit provides automatic model auditing for Laravel apps, tracking create, update, and delete events so you can see who changed what and when. It stores revisions and lets you review or revert changes.

View on GitHub
Deep Wiki
Context7

Getting Started

The package is published as owen-it/laravel-auditing (the jarek-paprocki/laravel-audit name in the brief points at the same codebase/lineage). It records model change history into an audits table by hooking Eloquent events, and gives you an API to read old/new values, who did it, and when.

Install and publish:

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

Auto-discovery handles the service provider on modern Laravel; older versions need OwenIt\Auditing\AuditingServiceProvider registered in config/app.php.

Where to look first:

  • config/audit.php — driver, audit implementation, user resolver, console behaviour, timeline, strict mode.
  • The published create_audits_table migration — check the JSON columns and indexes before you have data.
  • The Auditable trait — this is where the event hooks live; skim bootAuditable() to understand exactly when an audit is written.

First use case — audit an order:

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

class Order extends Model implements Auditable
{
    use AuditableTrait;
}
$order->update(['status' => 'paid']);

$audit = $order->audits()->latest()->first();

$audit->event;            // "updated"
$audit->getModified();    // ['status' => ['old' => 'pending', 'new' => 'paid']]
$audit->getMetadata();    // user, ip, user_agent, url, created_at

That getModified() shape is what you render in an admin timeline or feed to an external compliance store.

Implementation Patterns

Allow-list the attributes you actually care about. Most real apps don't want the full attribute payload (base64 blobs, API tokens, internal flags). Use include/exclude lists rather than auditing everything:

protected $auditInclude = ['status', 'total', 'assigned_to'];
// or
protected $auditExclude = ['api_token', 'last_seen_at'];

Combine with protected $auditStrict = true; so an audit is only written when one of the included attributes changed — this prevents noise from touch() and incidental updates.

Control which events are recorded per model. Defaults are created/updated/deleted/restored. Override when needed:

protected $auditEvents = ['created', 'updated', 'deleted'];

For one-off operations you can switch the event name on the instance ($model->auditEvent = 'approved') and toggle auditing with Order::disableAuditing() / Order::enableAuditing().

Shape the payload once, centrally. Override transformAudit() to add tenant/request context columns, and resolveData() if you need to normalise values before they are persisted:

public function transformAudit(array $data): array
{
    $data['tenant_id'] = tenant('id');
    return $data;
}

Do this in a shared base model or trait so every auditable model behaves identically.

Handle sensitive attributes with modifiers. Instead of excluding a field entirely, store a hash or redacted value:

protected $attributeModifiers = [
    'email' => fn ($value) => $value ? sha1($value) : null,
];

Resolve "who" correctly. The default resolver reads the authenticated user from configured guards. In API/token contexts, impersonation flows, or multi-guard apps, replace it in config/audit.php:

'resolver' => [
    'user' => fn () => auth()->user() ?? request()->user(),
],

If you use morph maps, also verify user_type on the audit matches the alias — otherwise historical rows become inconsistent after a refactor.

Use tags for grouping and filtering. Tag audits by workflow (billing, approval) via generateTags() or the config tags array, then filter admin UI queries: Audit::where('tags', 'like', '%billing%').

Fan out with events, don't put side effects in the model. Listen to OwenIt\Auditing\Events\Audited to push to a SIEM, broadcast, or queue a notification. The Auditing event fires before the insert and can veto an audit by returning false.

Swap the audit model or driver for scale. Set config('audit.implementation') to your own model implementing OwenIt\Auditing\Contracts\Audit to add columns or soft deletes. For high write volume, implement OwenIt\Auditing\Contracts\AuditDriver and ship writes to a separate store.

Query patterns for reporting. JSON path queries work on MySQL/Postgres via Laravel's -> syntax:

Audit::where('auditable_type', Order::class)
    ->where('new_values->status', 'paid')
    ->latest()
    ->paginate(50);

Eager load audits.auditable for admin tables, and prefer withCount('audits') over loading full audit collections in list views.

Gotchas and Tips

  • Bulk updates bypass auditing. Model::where(...)->update(...), DB::table(), and raw SQL don't fire model events. If compliance requires coverage, force per-model saves or add a separate audit path.
  • saveQuietly() / updateQuietly() skip auditing — they suppress model events by design. Audit critical workflows with regular saves.
  • Morph map drift is the most common data corruption. Set Relation::enforceMorphMap() before recording any audits. Renaming a model class after the fact orphans every historical auditable_type; plan for a migration if you must rename.
  • Console and queue contexts have no authenticated user by default. config('audit.console') is false; if you need audits from jobs, set a resolver that reads an explicitly passed user ID rather than relying on auth().
  • updated audits only fire when attributes are dirty. touch(), timestamp-only updates, or saving unchanged models produce nothing. Set $auditTimestamps = true if you want updated_at included as a modified attribute.
  • The created audit has empty old_values; deleted has only old values. UI code that assumes both sides exist will break.
  • Pivot and belongsToMany changes are not audited. Use ->using(PivotModel::class) with the trait, or record a synthetic audit entry.
  • JSON columns need MySQL 5.7+ / MariaDB 10.2+. On older engines, edit the published migration to use text. Watch max_allowed_packet if you audit large payloads.
  • The audits table grows monotonically. Add pruning or archiving early. $auditThreshold caps audits per model by deleting the oldest — convenient, but it silently destroys history. Prefer a scheduled archive job for anything compliance-adjacent.
  • $auditStrict and $auditInclude interact. Strict mode compares dirty keys against the include list; an update that changes only excluded attributes is discarded. Test this explicitly.
  • Audits are written synchronously inside the model save, so they participate in the surrounding transaction. Custom drivers and queued listeners break that atomicity — decide whether a failed audit should roll back the business write.
  • Changing implementation or the audit model class does not migrate old rows. Any column your custom model expects must exist before you swap it in.
  • config:cache after changing config/audit.php. A resolver closure in the config file is fine, but stale caches are a frequent "why is user_id null" cause.
  • For tests, disable auditing in the suite (Order::disableAuditing() in setUp) unless the audit itself is under test; otherwise factory-heavy tests produce large, meaningless audit tables and slow down RefreshDatabase.
  • Indexes matter. Ensure auditable_type/auditable_id, user_id, event, and created_at are indexed; timeline queries are almost always "for this record, newest first".
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.
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox