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::configphp artisan vendor:publish --tag=wire-core::migrationsphp 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']); // recordedOrder::query()->where('region', 'EU')->update([...]); // not recordedOrder::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.