⌘K

Core

Audit Log

The trail wire-core records for you — which events, on which models, with the before and after of every field that moved.

On this page

Wire Core includes an audit log for recording model changes and table-related events. It stores the event type, auditable record, user, old values, new values, metadata, and timestamp.

Install

The audit log is part of wire-core. If you installed wire-table or wire-forms, core is already installed.

Publish the config and migration:

php artisan vendor:publish --tag=wire-core::config
php artisan vendor:publish --tag=wire-core::migrations
php artisan migrate

That's the whole setup — the package registers the audit event subscriber automatically, and the logger is gated by wire-core.audit.enabled (on by default). If you registered AuditEventSubscriber manually in an application service provider (the pre-1.7.1 setup), you can remove that line; the subscription is idempotent, so keeping it does not double-log.

Enable Auditing On A Model

Add HasAuditable to any Eloquent model you want to track.

use Illuminate\Database\Eloquent\Model;
use NyonCode\WireCore\Audit\Concerns\HasAuditable;
 
class Order extends Model
{
use HasAuditable;
}

The trait records Eloquent created, updated, and deleted events.

It sees what the model sees, and nothing else. A write made through the query builder fires no model event — that is Laravel's design, not this package's — so it leaves no entry:

$order->update(['status' => 'paid']); // recorded
Order::query()->where('region', 'EU')->update([...]); // not recorded
Order::query()->where('status', 'draft')->delete(); // not recorded

The same goes for insert(), upsert() and increment() on a query. When the trail has to hold such a write, loop the models, or raise the entry yourself with one of the manual audit events. Soft deletes need neither: restore() saves, so it is an updated entry with deleted_at going back to null, and forceDelete() is a deleted one.

Exclude Or Include Columns

Use getAuditExclude() to hide noisy or sensitive columns for a single model.

class Order extends Model
{
use HasAuditable;
 
protected function getAuditExclude(): array
{
return ['cached_total', 'internal_token'];
}
}

Use getAuditInclude() when you want a whitelist.

protected function getAuditInclude(): array
{
return ['status', 'total', 'assigned_user_id'];
}

Global exclusions live in config/wire-core.php, and * is allowed:

'audit' => [
'exclude_columns' => [
'password',
'remember_token',
'billing_*',
],
],

This list adds to a floor; it does not define one. Passwords, anything ending in _token or _secret, two-factor columns, recovery codes and api_key are never written to the trail, and emptying the list does not bring them back. The trail is long-lived by default and the audit module renders old and new values on a screen — so a credential reaching it is a credential on a page, and a published config file from three years ago must not be what stands between you and that.

View Audit Entries

Each auditable model gets an audits() relation.

$entries = $order->audits()->latest()->get();

Audit entries expose helpers for common queries:

use NyonCode\WireCore\Audit\AuditEntry;
 
AuditEntry::forRecord($order)->get();
AuditEntry::forEvent('updated')->get();
AuditEntry::byUser($user->id)->get();
AuditEntry::olderThan(90)->delete();

To show a row-level audit trail in a Wire Table, add the built-in action:

use NyonCode\WireCore\Audit\Actions\AuditTrailAction;
 
return $table
->model(Order::class)
->actions([
AuditTrailAction::make(),
]);

The action opens a slide-over with the record history.

Manual Audit Events

You can dispatch audit events manually for operations that do not go through an audited model event.

Five events reach the logger, and the first three are what HasAuditable fires for you — dispatch one yourself when a write happened somewhere the model events did not see it (a raw query, an import, a state change made by a job):

Event Constructor Fired for you by
RecordCreated (Model $record, array $newValues = [], array $metadata = []) HasAuditable, on create
RecordUpdated (Model $record, array $oldValues = [], array $newValues = [], array $metadata = []) HasAuditable, on update
RecordDeleted (Model $record, array $oldValues = [], array $metadata = []) HasAuditable, on delete
BulkActionExecuted (string $actionName, string $modelType, array $recordIds, bool $success, array $metadata = []) nothing — yours to raise
InlineCellUpdated see below the table's inline edit path
use NyonCode\WireCore\Audit\Events\BulkActionExecuted;
 
event(new BulkActionExecuted(
actionName: 'archive',
modelType: Order::class,
recordIds: $orders->modelKeys(),
success: true,
metadata: ['source' => 'orders-table'],
));

For a single cell update:

use NyonCode\WireCore\Audit\Events\InlineCellUpdated;
 
event(new InlineCellUpdated(
modelType: Order::class,
recordId: $order->id,
column: 'status',
oldValue: 'draft',
newValue: 'approved',
));

Disable Temporarily

Disable audit logging during imports, seeders, or maintenance jobs:

use NyonCode\WireCore\Audit\AuditLogger;
 
AuditLogger::withoutAuditing(function () use ($orders) {
$orders->each->update(['synced_at' => now()]);
});

It silences model writes — the ones that would otherwise be recorded. A query builder update() needs no wrapping, because it was never going to be.

Retention

Set a retention period in days:

'audit' => [
'retention_days' => 180,
],

Then schedule the bundled prune command:

use Illuminate\Support\Facades\Schedule;
 
Schedule::command('wire-core:audit-prune')->daily();

Run it manually with an ad-hoc period override:

php artisan wire-core:audit-prune --days=90

Without a configured retention_days (and no --days), the command warns and prunes nothing. Programmatic pruning is still available via app(AuditLogger::class)->prune(?int $days = null).

A period that keeps nothing is refused. --days=0 would be "everything written before this second" and a negative one reaches into the future, so both — and an empty --days= from a scheduler variable that was never set — exit non-zero with nothing deleted. prune() throws InvalidRetentionException for the same values, and a retention_days of 0 is refused the same way. A scheduled prune that fails loudly gets noticed; one that wipes the trail and reports success does not.

Configuration

Key Default Description
enabled true Global on/off switch
model AuditEntry::class Custom audit entry model
user_model App\Models\User User model for the user() relation
events null null logs all supported events; array logs only selected event types
exclude_columns password, remember_token Global column exclusions, * allowed — added to a built-in floor that always redacts credentials
retention_days null Number of days to keep entries

Supported event types are created, updated, deleted, bulk_action, and cell_updated.

See Configuration for the full config reference.