PHPackages                             softartisan/laravel-model-audits - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. softartisan/laravel-model-audits

Abandoned → [softartisan/laravel-audit-events](/?search=softartisan%2Flaravel-audit-events)Library[Database &amp; ORM](/categories/database)

softartisan/laravel-model-audits
================================

Tamper-evident audit trail for Laravel: automatic Eloquent model auditing, free-standing semantic events, an HMAC hash-chain integrity guarantee, and cold archiving for long-term compliance.

v2.1.0(3w ago)0381↓90.1%[2 PRs](https://github.com/softartisan-inc/laravel-audit-events/pulls)MITPHPPHP ^8.2CI passing

Since Dec 6Pushed 1w agoCompare

[ Source](https://github.com/softartisan-inc/laravel-audit-events)[ Packagist](https://packagist.org/packages/softartisan/laravel-model-audits)[ Docs](https://github.com/softartisan-inc/laravel-audit-events)[ GitHub Sponsors](https://github.com/softartisan)[ RSS](/packages/softartisan-laravel-model-audits/feed)WikiDiscussions main Synced yesterday

READMEChangelog (3)Dependencies (29)Versions (10)Used By (0)

Laravel Audit Events
====================

[](#laravel-audit-events)

[![Latest Version on Packagist](https://camo.githubusercontent.com/8d0ed42d25ebe05b2524ec34a389cb5d19cb451a3c755a98411d145e17b4ce87/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f736f66746172746973616e2f6c61726176656c2d61756469742d6576656e74732e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/softartisan/laravel-audit-events)[![Tests](https://camo.githubusercontent.com/5f63fa63f922f5ad8648bac006da0bbf53b78cc1b6a6b401a0779d537a409103/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f736f66746172746973616e2d696e632f6c61726176656c2d61756469742d6576656e74732f72756e2d74657374732e796d6c3f6272616e63683d6d61696e266c6162656c3d7465737473267374796c653d666c61742d737175617265)](https://github.com/softartisan-inc/laravel-audit-events/actions/workflows/run-tests.yml)[![Total Downloads](https://camo.githubusercontent.com/11644a97886257a93c5f4076dc28640578e617f404230b8312ee1f57c17e29f6/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f736f66746172746973616e2f6c61726176656c2d61756469742d6576656e74732e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/softartisan/laravel-audit-events)

A production-ready Laravel package that **automatically audits Eloquent model changes**, records **free-standing semantic events**, guarantees **cryptographic integrity** of every audit record, and provides a **cold archiving** strategy for long-term retention — designed for ERP systems and compliance-sensitive applications.

### Why another audit package?

[](#why-another-audit-package)

`spatie/laravel-activitylog` and `owen-it/laravel-auditing` are excellent and, for most apps, the right choice. Reach for **this** package when you specifically need:

- **Tamper-evidence** — an HMAC hash-chain per record with a `verify` command, not just a log table anyone with DB access can silently edit. ([threat model](./SECURITY.md))
- **One model for everything** — automatic model audits **and** free-standing semantic events (`user.logged_in`, `csv.exported`, `impersonation.started`) in a single, queryable table.
- **Compliance retention built in** — cold archiving to a DB table or JSONL files, per-tenant retention, and pruning.
- **Multi-tenant by accident, not by coupling** — writes to the current connection, zero dependency on any tenancy package (works the same in a plain Laravel app).

If you don't need integrity guarantees or free-standing events, the established packages are lighter. We'd rather you pick the right tool.

> **v2.0 — breaking changes**: Package renamed from `softartisan/laravel-model-audits` to `softartisan/laravel-audit-events`. See the [Upgrade Guide](#upgrade-from-v1x) below.

> 📖 **Looking for "how do I do X?"** See the scenario-driven **[Use-Case Cookbook → `USE-CASES.md`](./USE-CASES.md)** — every use case (auto-audit, manual/free events, jobs, revert, export, multi-tenant isolation, impersonation, integrity, retention, frontend rendering…) with copy-paste code.

---

Features
--------

[](#features)

- Automatic audit trail for `created`, `updated`, `deleted`, `restored` Eloquent events
- `AuditContext::actingAs()` — inject the causer in queue jobs where `Auth::id()` is `null`
- `ModelAudit::record()` — record free-standing events (login, export, PDF…) without an Eloquent anchor
- `saveHistory()` — manually record a semantic event bound to a specific model
- `TracksRelationChanges` — track pivot/sync relation changes (many-to-many)
- Deep JSON diff — recursively diff array/JSON fields to pinpoint sub-key changes
- Global + per-model attribute masking (passwords, tokens, credit cards…)
- **Cryptographic integrity** — HMAC-SHA256 signature + hash chain per model, tamper-evident audit trail
- **Cold archiving** — move old records to a dedicated table or daily JSONL files instead of deleting them
- Configurable pruning with per-tenant retention
- `audit-events:stats` — audit statistics at a glance
- `audit-events:verify` — bulk integrity verification
- `audit-events:archive` — archive old records to cold storage
- MCP server integration (optional, requires `laravel/mcp`)
- PHP 8.4 · Laravel 12 · Pest · PHPStan level 5

---

Table of Contents
-----------------

[](#table-of-contents)

- [Installation](#installation)
- [Upgrade from v1.x](#upgrade-from-v1x)
- [Basic Usage](#basic-usage)
- [Querying Audit History](#querying-audit-history)
- [Restore a Model](#restore-a-model)
- [Attribute Masking](#attribute-masking)
- [AuditContext — Queue Jobs](#auditcontext--queue-jobs)
- [Free Events — ModelAudit::record()](#free-events--modelauditrecord)
- [saveHistory() — Manual Model Events](#savehistory--manual-model-events)
- [TracksRelationChanges](#tracksrelationchanges)
- [Deep JSON Diff](#deep-json-diff)
- [Cryptographic Integrity](#cryptographic-integrity)
- [Cold Archiving](#cold-archiving)
- [Pruning](#pruning)
- [Artisan Commands](#artisan-commands)
- [Configuration Reference](#configuration-reference)
- [Testing](#testing)
- [Changelog](#changelog)

---

Installation
------------

[](#installation)

```
composer require softartisan/laravel-audit-events
```

Publish the config and run the migrations:

```
php artisan vendor:publish --tag="laravel-audit-events-config"
php artisan migrate
```

---

Upgrade from v1.x
-----------------

[](#upgrade-from-v1x)

> **Breaking changes** in v2.0:
>
> - Package renamed: `softartisan/laravel-model-audits` → `softartisan/laravel-audit-events`
> - Namespace changed: `SoftArtisan\LaravelModelAudits` → `SoftArtisan\LaravelAuditEvents`
> - Config key changed: `model-audits` → `audit-events`
> - Artisan command renamed: `model-audits:stats` → `audit-events:stats`
> - Audit table renamed: `model_audits` → `audit_events`

**Step 1** — update your `composer.json`:

```
composer require softartisan/laravel-audit-events:^2.0
```

**Step 2** — update all `use` statements and config references in your app:

```
// Before
use SoftArtisan\LaravelModelAudits\Concerns\IsAuditable;
config('model-audits.table_name');

// After
use SoftArtisan\LaravelAuditEvents\Concerns\IsAuditable;
config('audit-events.table_name');
```

**Step 3** — publish the new config and run migrations:

```
php artisan vendor:publish --tag="laravel-audit-events-config"
php artisan migrate
```

The bundled `rename_model_audits_to_audit_events_table` migration renames `model_audits → audit_events` safely (checks table existence before acting). To keep the old table name, override the config:

```
// config/audit-events.php
'table_name' => 'model_audits',
```

---

Basic Usage
-----------

[](#basic-usage)

### Attach `IsAuditable` to a model

[](#attach-isauditable-to-a-model)

```
use SoftArtisan\LaravelAuditEvents\Concerns\IsAuditable;

class Invoice extends Model
{
    use IsAuditable;
}
```

Every `created`, `updated`, `deleted`, and `restored` event now produces an audit record automatically.

---

Querying Audit History
----------------------

[](#querying-audit-history)

```
$invoice->audits()->get();               // all audits for this model
$invoice->audits()->latest()->first();   // most recent audit

// Filtered by event type
$invoice->getCreatedHistory()->get();
$invoice->getUpdatedHistory()->get();
$invoice->getDeletedHistory()->get();
$invoice->getRestoredHistory()->get();
$invoice->getAuditHistory('invoice.sent')->get(); // any event name

// Diff between old and new values on an update audit
$audit = $invoice->getUpdatedHistory()->latest()->first();
$diff  = $audit->getDiff();
// ['amount' => ['old' => 100, 'new' => 250]]
```

### Query scopes on `ModelAudit`

[](#query-scopes-on-modelaudit)

For querying across the whole `audit_events` table (not just one model's `audits()`relation), three local scopes are available:

```
use SoftArtisan\LaravelAuditEvents\Models\ModelAudit;

// Filter by event name (CRUD or semantic).
ModelAudit::whereEvent('asset.status_changed')->get();

// Filter by a key inside the JSON `context` column (portable across
// MySQL / PostgreSQL / SQLite via Laravel's `->` JSON path operator).
ModelAudit::whereContext('mission_id', 42)->get();

// Filter by the anchored model instance (uses the indexed morph columns).
ModelAudit::forAuditable($invoice)->get();

// Compose them freely.
ModelAudit::forAuditable($asset)
    ->whereEvent('asset.status_changed')
    ->whereContext('mission_id', 42)
    ->latest('created_at')
    ->get();
```

---

Restore a Model
---------------

[](#restore-a-model)

Roll back a model to the state stored in a previous audit's `old_values`:

```
$audit = $invoice->audits()->latest()->first();
$audit->restore(); // forceFills old_values back onto the model and saves
```

Columns that no longer exist in the table are silently skipped.

---

Attribute Masking
-----------------

[](#attribute-masking)

**Globally** in `config/audit-events.php`:

```
'global_hidden' => [
    'password',
    'password_confirmation',
    'remember_token',
    'secret',
    'credit_card_number',
],
```

**Per model** — override `getHiddenForAudit()`:

```
class Patient extends Model
{
    use IsAuditable;

    public function getHiddenForAudit(): array
    {
        return array_merge(parent::getHiddenForAudit(), [
            'ssn',
            'date_of_birth',
            'medical_record_number',
        ]);
    }
}
```

Masked attributes are stripped from both `old_values` and `new_values` before storage.

---

AuditContext — Queue Jobs
-------------------------

[](#auditcontext--queue-jobs)

In queue jobs, `Auth::id()` is `null` because there is no active session. Use `AuditContext::actingAs()` to inject the causer manually.

```
use SoftArtisan\LaravelAuditEvents\AuditContext;

class ImportInvoicesJob implements ShouldQueue
{
    public function __construct(
        private readonly int $userId,
        private readonly string $batchId,
    ) {}

    public function handle(): void
    {
        AuditContext::actingAs($this->userId, [
            'source'   => 'import-job',
            'batch_id' => $this->batchId,
        ]);

        // All audits produced here will carry $this->userId as causer
        Invoice::create([...]);
        Invoice::find(42)->update([...]);

        AuditContext::reset(); // Always reset — prevents context bleed
    }
}
```

`AuditContext` is a static class. Reset is mandatory at the end of every job because PHP-FPM/Swoole workers reuse the same process.

---

Free Events — ModelAudit::record()
----------------------------------

[](#free-events--modelauditrecord)

Record semantic events that have no Eloquent model anchor:

```
use SoftArtisan\LaravelAuditEvents\Models\ModelAudit;

// Authentication events
ModelAudit::record('user.logged_in',  ['ip' => request()->ip()], $user->id);
ModelAudit::record('user.logged_out', [], $user->id);

// Bulk operations
ModelAudit::record('csv.exported', [
    'resource' => 'fixed-assets',
    'count'    => 1500,
    'tenant'   => tenant('id'),
], $this->userId);

// Report generation
ModelAudit::record('pdf.generated', [
    'template'   => 'annual-report',
    'invoice_id' => $invoice->id,
], auth()->id());
```

Signature:

```
ModelAudit::record(
    string          $event,
    array           $context   = [],
    int|string|null $causerId  = null,
): ModelAudit
```

Free events are **never** filtered by the events whitelist.

---

saveHistory() — Manual Model Events
-----------------------------------

[](#savehistory--manual-model-events)

Record a custom semantic event bound to a specific model instance:

```
// Invoice sent to client
$invoice->saveHistory(
    event:     'invoice.sent',
    oldValues: [],
    newValues: [],
    context:   ['recipient' => 'client@example.com', 'channel' => 'email'],
);

// Status transition with diff
$invoice->saveHistory(
    event:     'invoice.approved',
    oldValues: ['status' => 'draft'],
    newValues: ['status' => 'approved'],
    context:   ['approver_id' => auth()->id()],
);
```

Signature:

```
public function saveHistory(
    string $event,
    array  $oldValues = [],
    array  $newValues = [],
    array  $context   = [],
): void
```

Not subject to the events whitelist.

---

TracksRelationChanges
---------------------

[](#tracksrelationchanges)

Laravel does not emit Eloquent events when pivot tables are modified (`sync`, `attach`, `detach`). Use this trait alongside `IsAuditable` to track those changes manually.

```
use SoftArtisan\LaravelAuditEvents\Concerns\IsAuditable;
use SoftArtisan\LaravelAuditEvents\Concerns\TracksRelationChanges;

class Role extends Model
{
    use IsAuditable, TracksRelationChanges;
}
```

```
class RoleService
{
    public function syncPermissions(Role $role, array $permissionIds): void
    {
        $before = $role->permissions->pluck('name')->all();

        $role->permissions()->sync($permissionIds);

        $after = $role->fresh()->permissions->pluck('name')->all();

        $role->recordRelationAudit('permissions', $before, $after, [
            'actor_id' => auth()->id(),
        ]);
    }
}
```

The audit record event will be `relation.synced`. The `old_values` and `new_values` are keyed by the relation name:

```
{
  "old_values": { "permissions": ["read", "write"] },
  "new_values": { "permissions": ["read", "write", "delete"] }
}
```

---

Deep JSON Diff
--------------

[](#deep-json-diff)

When a model has a JSON/array column, `getDiff()` recursively diffs it to pinpoint exact sub-key changes:

```
// Before: settings = ['theme' => 'light', 'notifications' => ['email' => true, 'sms' => false]]
// After:  settings = ['theme' => 'dark',  'notifications' => ['email' => true, 'sms' => true]]

$diff = $audit->getDiff();
// [
//   'settings' => [
//     'old'  => ['theme' => 'light', 'notifications' => ['email' => true, 'sms' => false]],
//     'new'  => ['theme' => 'dark',  'notifications' => ['email' => true, 'sms' => true]],
//     'diff' => [
//       'theme'         => ['old' => 'light', 'new' => 'dark'],
//       'notifications' => [
//         'old'  => ['email' => true, 'sms' => false],
//         'new'  => ['email' => true, 'sms' => true],
//         'diff' => ['sms' => ['old' => false, 'new' => true]],
//       ],
//     ],
//   ],
// ]
```

Configure in `config/audit-events.php`:

```
'json_diff' => [
    'enabled'   => true,
    'max_depth' => 3,
],
```

---

Cryptographic Integrity
-----------------------

[](#cryptographic-integrity)

The integrity feature adds a tamper-evident HMAC-SHA256 signature to every audit record, plus a hash chain that links each record to its predecessor within the same model's history.

### Setup

[](#setup)

**Step 1** — run the migration:

```
php artisan migrate
# Applies: add_signature_to_audit_events_table
```

**Step 2** — enable in `config/audit-events.php`:

```
'integrity' => [
    'enabled'   => true,
    'key'       => null,       // null uses APP_KEY. Set a dedicated AUDIT_SIGNING_KEY for isolation.
    'algorithm' => 'sha256',   // Any PHP hash_hmac() algorithm
],
```

**Step 3** (optional) — set a dedicated signing key in `.env`:

```
AUDIT_SIGNING_KEY=base64:your-32-byte-key-here
```

Then reference it in the published config:

```
'integrity' => [
    'enabled' => true,
    'key'     => env('AUDIT_SIGNING_KEY'),
],
```

### How it works

[](#how-it-works)

Each new audit record receives:

- **`signature`** (varchar 64): HMAC over a canonical JSON payload covering `auditable_type`, `auditable_id`, `event`, `user_id`, `old_values`, `new_values`, `context`, `created_at`, and `previous_hash`.
- **`previous_hash`** (varchar 64): the `signature` of the immediately preceding record for the same `(auditable_type, auditable_id)` pair. `null` for the first record.

The hash chain scope is per model instance — two different `Invoice` records maintain independent chains, avoiding write contention. Free-standing events (no auditable) are chained by `user_id`.

### Verifying a single record

[](#verifying-a-single-record)

```
$audit = Invoice::find(1)->audits()->latest()->first();

$audit->isSigned();       // true if the record has a non-null signature
$audit->verifySignature(); // true if the HMAC matches; false if tampered
```

### Bulk verification

[](#bulk-verification)

```
php artisan audit-events:verify

# Limit to a model
php artisan audit-events:verify --model="App\Models\Invoice"

# Limit to one instance
php artisan audit-events:verify --model="App\Models\Invoice" --id=42

# Date range
php artisan audit-events:verify --from=2025-01-01 --until=2025-12-31

# Stop on first failure
php artisan audit-events:verify --fail-fast
```

Exit codes: `0` = all signed records passed · `1` = tampered records found (or integrity disabled).

Records created before the feature was enabled are reported as **unsigned** (not tampered) and do not affect the exit code.

### Key management

[](#key-management)

- Use a **dedicated key** (`AUDIT_SIGNING_KEY`) separate from `APP_KEY` so that rotating your app key does not invalidate existing audit signatures.
- Store the key in a secrets manager (AWS Secrets Manager, HashiCorp Vault). Do not store it only in `.env` for compliance-critical applications.
- If you must rotate the signing key, re-sign historical records via a one-off artisan command (not provided — implement in your app with `AuditSignatureService`).

---

Cold Archiving
--------------

[](#cold-archiving)

Archiving moves records older than a configurable threshold to cold storage, preserving them for legal/compliance purposes while keeping the hot `audit_events` table lean.

### Setup

[](#setup-1)

**Step 1** — run the migration (database driver):

```
php artisan migrate
# Applies: create_audit_events_archive_table
```

**Step 2** — enable in `config/audit-events.php`:

```
'archive' => [
    'enabled'            => true,
    'archive_after_days' => 90,         // Records older than 90 days
    'driver'             => 'database', // 'database' | 'json_file'
    'table_name'         => 'audit_events_archive',
    'path'               => null,       // Required for json_file driver; null = storage_path('audit-archives')
],
```

**Step 3** — schedule the archive command:

```
// bootstrap/app.php
use Illuminate\Console\Scheduling\Schedule;

->withSchedule(function (Schedule $schedule) {
    $schedule->command('audit-events:archive')->weekly();
})
```

### Database driver

[](#database-driver)

Moves records in transactional batches (default 500/batch). Each batch:

1. Bulk-inserts into `audit_events_archive` (with `archived_at` timestamp)
2. Deletes from `audit_events` — only if the insert succeeded

The archive table has an identical schema to `audit_events`, plus `archived_at`. Signatures and hash chain columns (`signature`, `previous_hash`) are preserved.

### JSON file driver

[](#json-file-driver)

Appends records to daily JSONL files (one JSON object per line):

```
storage/audit-archives/audit_events_archive_2025_03_29.jsonl
storage/audit-archives/audit_events_archive_2025_03_30.jsonl

```

Each line is a complete JSON representation of the audit record plus `archived_at`. Files can be gzipped and uploaded to S3 for long-term storage.

```
'archive' => [
    'enabled' => true,
    'driver'  => 'json_file',
    'path'    => storage_path('audit-archives'), // or /mnt/cold-storage
],
```

### Archive command options

[](#archive-command-options)

```
# Preview without changes
php artisan audit-events:archive --dry-run

# Override threshold
php artisan audit-events:archive --days=365

# Override driver
php artisan audit-events:archive --driver=json_file

# Limit to one model type
php artisan audit-events:archive --model="App\Models\Invoice"

# Custom batch size
php artisan audit-events:archive --chunk=1000
```

### Hash chain continuity after archiving

[](#hash-chain-continuity-after-archiving)

After archiving, the live table has a gap at the chain boundary. The **next** new audit record on the same model will reference the most recent *remaining live* record's signature as its `previous_hash`. The archived record retains its signature intact and can be cross-referenced manually.

When running `audit-events:verify`, a chain break at an archive boundary is expected. The verify command reports it as a gap rather than tampering.

---

Pruning
-------

[](#pruning)

Pruning **deletes** records permanently. Use it for data that has no legal retention obligation.

```
'pruning' => [
    'enabled'      => true,
    'keep_for_days' => 365,
],
```

When `enabled` is `true`, the service provider auto-schedules `model:prune` daily. You can also schedule it manually:

```
Schedule::command('model:prune', [
    '--model' => [\SoftArtisan\LaravelAuditEvents\Models\ModelAudit::class],
])->daily();
```

### Multi-tenant retention

[](#multi-tenant-retention)

`keep_for_days` is read dynamically at every pruning run (never cached), so multi-tenant applications can set different retention periods per tenant:

```
// Tenant A — standard
config(['audit-events.pruning.keep_for_days' => 365]);

// Tenant B — financial compliance (7 years)
config(['audit-events.pruning.keep_for_days' => 2555]);
```

### Pruning vs. Archiving

[](#pruning-vs-archiving)

PruningArchivingRecords after operationDeleted permanentlyPreserved in cold storageCompliance-safeOnly if retention period metYesHash chainBroken at deletionIntact in archiveRecommended forNon-sensitive operational dataFinancial, medical, legal recordsUse **pruning** for high-volume, low-sensitivity events. Use **archiving** when records must be retained for legal or compliance reasons.

---

Artisan Commands
----------------

[](#artisan-commands)

### `audit-events:stats`

[](#audit-eventsstats)

Display audit event statistics.

```
php artisan audit-events:stats
```

Output includes: total records, breakdown by event type, top 5 audited model classes, date range, table size (MySQL/PostgreSQL), and archive stats when `archive.enabled = true`.

---

### `audit-events:verify`

[](#audit-eventsverify)

Verify the cryptographic integrity of audit records. Requires `integrity.enabled = true`.

```
php artisan audit-events:verify [options]

Options:
  --model=CLASS    Limit to a specific auditable_type (FQCN)
  --id=ID          Limit to a specific auditable_id (requires --model)
  --from=DATE      Verify records created on or after this date (Y-m-d)
  --until=DATE     Verify records created on or before this date (Y-m-d)
  --fail-fast      Stop at the first failure
```

Exit codes: `0` = pass · `1` = tampered records or feature disabled.

---

### `audit-events:archive`

[](#audit-eventsarchive)

Move old audit records to cold storage.

```
php artisan audit-events:archive [options]

Options:
  --days=N         Archive records older than N days (overrides config)
  --driver=NAME    Use 'database' or 'json_file' (overrides config)
  --dry-run        Show what would be archived without moving records
  --chunk=N        Records per batch (default: 500)
  --model=CLASS    Limit to a specific auditable_type (FQCN)
```

---

### `audit-events:stats` (archive section)

[](#audit-eventsstats-archive-section)

When `archive.enabled = true`, the stats command adds an archive section:

```
Archive
  Archived records : 18 432
  Oldest archived  : 2024-01-03 09:12:00
  Newest archived  : 2025-12-31 23:59:00

```

---

Configuration Reference
-----------------------

[](#configuration-reference)

```
// config/audit-events.php

return [

    // ── Database ──────────────────────────────────────────────────────────────

    'table_name'  => 'audit_events',
    'model_class' => \SoftArtisan\LaravelAuditEvents\Models\ModelAudit::class,

    // ── Column Mapping ────────────────────────────────────────────────────────
    //
    // morph_type options: 'string' (recommended), 'integer', 'uuid', 'ulid'

    'table_fields' => [
        'id'           => 'audit_id',
        'user_id'      => 'user_id',
        'event'        => 'event',
        'morph_prefix' => 'auditable',
        'morph_type'   => 'string',
        'url'          => 'url',
        'ip_address'   => 'ip_address',
        'user_agent'   => 'user_agent',
        'old_values'   => 'old_values',
        'new_values'   => 'new_values',
        'context'      => 'context',
    ],

    // ── Behaviour ─────────────────────────────────────────────────────────────

    'audit_on_create'  => true,
    'audit_on_update'  => true,

    // true  → remove all audits when a model is hard-deleted
    // false → keep audits and record a final "deleted" entry
    'remove_on_delete' => true,

    // Automatic Eloquent events whitelist.
    // saveHistory() and ModelAudit::record() always bypass this list.
    'events' => ['created', 'updated', 'deleted', 'restored'],

    // ── Security ──────────────────────────────────────────────────────────────

    'global_hidden' => [
        'password',
        'password_confirmation',
        'remember_token',
        'secret',
        'credit_card_number',
    ],

    // ── Deep JSON Diff ────────────────────────────────────────────────────────

    'json_diff' => [
        'enabled'   => true,
        'max_depth' => 3,
    ],

    // ── User Resolver ─────────────────────────────────────────────────────────

    'user' => [
        'guards'   => ['web', 'api', 'sanctum'],
        'resolver' => null, // callable — return the authenticated user instance
    ],

    // ── Pruning ───────────────────────────────────────────────────────────────

    'pruning' => [
        'enabled'       => false,
        'keep_for_days' => 365,
    ],

    // ── Cryptographic Integrity ───────────────────────────────────────────────

    'integrity' => [
        'enabled'   => false,
        'key'       => null,       // null falls back to APP_KEY
        'algorithm' => 'sha256',
    ],

    // ── Archiving ─────────────────────────────────────────────────────────────

    'archive' => [
        'enabled'            => false,
        'archive_after_days' => 90,
        'driver'             => 'database', // 'database' | 'json_file'
        'table_name'         => 'audit_events_archive',
        'path'               => null,       // null → storage_path('audit-archives')
    ],
];
```

---

Testing
-------

[](#testing)

```
composer test
```

Or directly with Pest:

```
./vendor/bin/pest
./vendor/bin/pest --coverage
```

Static analysis:

```
./vendor/bin/phpstan analyse --configuration phpstan.neon.dist --memory-limit=512M
```

### Testing in your application

[](#testing-in-your-application)

Disable integrity in tests to avoid `APP_KEY` dependency:

```
// tests/TestCase.php
protected function defineEnvironment($app): void
{
    $app['config']->set('audit-events.integrity.enabled', false);
}
```

Or enable it with a known key:

```
$app['config']->set('audit-events.integrity.enabled', true);
$app['config']->set('app.key', 'base64:'.base64_encode(str_repeat('x', 32)));
```

---

Changelog
---------

[](#changelog)

See [CHANGELOG.md](CHANGELOG.md).

License
-------

[](#license)

MIT. See [LICENSE.md](LICENSE.md).

###  Health Score

46

—

FairBetter than 92% of packages

Maintenance97

Actively maintained with recent releases

Popularity14

Limited adoption so far

Community9

Small or concentrated contributor base

Maturity53

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 93.8% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~62 days

Total

4

Last Release

21d ago

Major Versions

v1.1.1 → v2.1.02026-06-12

PHP version history (2 changes)v1.0.0PHP ^8.4

v2.1.0PHP ^8.2

### Community

Maintainers

![](https://www.gravatar.com/avatar/5a999c3cc0aa856366bd17f1f8ae4173c739c8a56f9b74fc6a34e6063dda7818?d=identicon)[softartisan](/maintainers/softartisan)

---

Top Contributors

[![henoc35](https://avatars.githubusercontent.com/u/13746485?v=4)](https://github.com/henoc35 "henoc35 (30 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (1 commits)")[![github-actions[bot]](https://avatars.githubusercontent.com/in/15368?v=4)](https://github.com/github-actions[bot] "github-actions[bot] (1 commits)")

---

Tags

laravelAuditevent sourcingaudit-trailactivity-logcomplianceaudit-logintegritysoftartisantamper-evidentlaravel-audit-events

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/softartisan-laravel-model-audits/health.svg)

```
[![Health](https://phpackages.com/badges/softartisan-laravel-model-audits/health.svg)](https://phpackages.com/packages/softartisan-laravel-model-audits)
```

###  Alternatives

[spatie/laravel-pdf

Create PDFs in Laravel apps

1.0k4.8M47](/packages/spatie-laravel-pdf)[wnx/laravel-backup-restore

A package to restore database backups made with spatie/laravel-backup.

213420.1k2](/packages/wnx-laravel-backup-restore)[rawilk/profile-filament-plugin

Profile &amp; MFA starter kit for filament.

3914.6k](/packages/rawilk-profile-filament-plugin)[mradder/filament-logger

Audit logging, activity tracking, exports, alerts, and dashboards for Filament admin panels.

2317.1k](/packages/mradder-filament-logger)[lacodix/laravel-model-filter

A Laravel package to filter, search and sort models with ease while fetching from database.

17558.6k](/packages/lacodix-laravel-model-filter)[harris21/laravel-fuse

Circuit breaker for Laravel queue jobs. Protect your workers from cascading failures.

44855.7k](/packages/harris21-laravel-fuse)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
